switch / pattern matching idea
我最近一直在关注F,虽然我不太可能很快就跨越这个障碍,但它肯定突出了一些C(或图书馆支持)可以让生活更轻松的领域。
特别是,我在考虑F的模式匹配能力,它允许非常丰富的语法-比当前的开关/条件C等价物更具表现力。我不会试图给出一个直接的例子(我的F做不到),但简而言之,它允许:
- 按类型匹配(对区分的联合进行完全覆盖检查)[注意,这还推断绑定变量的类型,提供成员访问等]
- 按谓词匹配
- 以上的组合(可能还有一些我不知道的其他场景)
虽然C最终借用这些丰富的内容是很有意思的,但在此期间,我一直在研究在运行时可以做些什么——例如,将一些对象组合在一起相当容易:
1 2 3 4 5 6 | var getRentPrice = new Switch<Vehicle, int>() .Case<Motorcycle>(bike => 100 + bike.Cylinders * 10) //"bike" here is typed as Motorcycle .Case<Bicycle>(30) // returns a constant .Case<Car>(car => car.EngineType == EngineType.Diesel, car => 220 + car.Doors * 20) .Case<Car>(car => car.EngineType == EngineType.Gasoline, car => 200 + car.Doors * 20) .ElseThrow(); // or could use a Default(...) terminator |
其中getRentPrice是func
[注-可能是交换/案例这是错误的术语…但它表明了这个想法]
对我来说,这比使用重复if/else或复合三元条件(对于非平凡表达式来说非常混乱,括号太多)的等价方法要清楚得多。它还避免了大量的强制转换,并允许简单的扩展(直接或通过扩展方法)到更具体的匹配,例如与vb select…case"x to y"用法类似的inrange(…)匹配。
我只是想评估一下人们是否认为像上面这样的结构有很多好处(在缺乏语言支持的情况下)?
另外,请注意,我一直在玩上述3种变体:
- 用于评估的func
版本-与复合三元条件语句相当 - 与if/else if/else if/else if/else if/else if/else相当的操作
- 表达式
>version-作为第一个,但可由任意LINQ提供程序使用
此外,使用基于表达式的版本可以重新编写表达式树,本质上是将所有分支嵌入到单个复合条件表达式中,而不是使用重复调用。我最近没有检查过,但是在一些早期的实体框架构建中,我似乎记得这是必要的,因为它不太喜欢invocationExpression。它还允许对linq-to对象更有效地使用,因为它避免了重复的委托调用-与等效的C复合条件语句相比,测试显示类似于上面的匹配(使用表达式形式)以相同的速度执行(实际上速度略快)。出于完整性考虑,基于func的版本花费了c条件语句4倍的时间,但仍然非常快速,在大多数用例中不太可能成为主要的瓶颈。
我欢迎任何关于以上(或更丰富的C语言支持的可能性)的想法/输入/评论/等。希望在这里;-p)。
巴特·德·斯密特的优秀博客有一个8部分的系列,讲述了你所描述的事情。在这里找到第一部分。
在试着用C语言做这种"功能性"的事情之后(甚至试着写一本关于它的书),我得出结论:不,除了少数例外,这样的事情没有太多帮助。
主要原因是,像f这样的语言从真正支持这些特性中获得了很大的能力。不是"你能做到",而是"很简单,很清楚,这是意料之中的"。
例如,在模式匹配中,编译器会告诉您是否存在不完整的匹配,或者何时将永远不会命中另一个匹配。这对于开放式类型不太有用,但是在匹配有区别的并集或元组时,它非常漂亮。在F中,您期望人们进行模式匹配,这一点立即变得有意义。
"问题"是,一旦你开始使用一些功能概念,你自然会想继续下去。然而,利用C中的元组、函数、部分方法应用和转换、模式匹配、嵌套函数、泛型、monad支持等,会很快变得非常难看。这很有趣,一些非常聪明的人用C语言做了一些非常酷的事情,但实际上使用它感觉很重。
我经常(跨项目)在C中使用的内容:
- 序列函数,通过IEnumerable的扩展方法。例如foreach或process("apply"?--对序列项执行操作,因为C语法很好地支持它。
- 抽象公共语句模式。复杂的try/catch/finally块或其他涉及的(通常是非常通用的)代码块。将Linq扩展到SQL也适用于这里。
- 在某种程度上,元组。
但请注意:缺少自动泛化和类型推断确实会妨碍这些特性的使用。**
正如其他人提到的,在一个小团队中,为了一个特定的目的,所有这些都说了,是的,如果你被C困住了,也许他们能帮上忙。但根据我的经验,他们通常觉得比自己的价值更麻烦。
其他一些链接:
- Mono.Rocks游乐场有许多类似的东西(以及非功能性编程,但有用的补充)。
- Luca Bolognese的功能C库
- Matthew Podwysocki在msdn上的功能C
可以说,C不能简单地打开类型是因为它主要是一种面向对象的语言,在面向对象的术语中,正确的方法是在车辆上定义一个getRentPrice方法,并在派生类中覆盖它。
也就是说,我花了一点时间在多范式和功能语言上玩,比如F和haskell,它们具有这种能力,我遇到过许多以前有用的地方(例如,当你不写你需要打开的类型,所以你不能在它们上实现一个虚拟方法),这是很重要的。我欢迎加入这门语言以及歧视工会。
[编辑:删除了Marc表示可能短路的性能部分]
另一个潜在的问题是可用性问题——从最后一次调用中可以清楚地看到,如果匹配不满足任何条件,会发生什么情况,但是如果匹配两个或更多条件,会发生什么行为?它应该引发异常吗?它应该返回第一个匹配还是最后一个匹配?
我解决此类问题的一种方法是使用一个字典字段,其中类型为键,lambda为值,这对于使用对象初始值设定项语法构造非常简单;但是,这只说明具体的类型,不允许附加谓词,因此可能不适用于更复杂的情况。[旁注-如果您查看C编译器的输出,它经常将switch语句转换为基于字典的跳转表,因此似乎没有很好的理由不支持切换类型]
我不认为这些类型的库(类似于语言扩展)有可能获得广泛的接受,但是它们很有趣,对于在特定领域工作的小团队来说非常有用。例如,如果您正在编写大量的"业务规则/逻辑",这些规则/逻辑执行像这样或那样的任意类型测试,那么我可以看到它是如何方便的。
我不知道这是否可能是一个C语言功能(似乎有疑问,但谁能看到未来?).
作为参考,相应的f约为:
1 2 3 4 5 6 7 | let getRentPrice (v : Vehicle) = match v with | :? Motorcycle as bike -> 100 + bike.Cylinders * 10 | :? Bicycle -> 30 | :? Car as car when car.EngineType = Diesel -> 220 + car.Doors * 20 | :? Car as car when car.EngineType = Gasoline -> 200 + car.Doors * 20 | _ -> failwith"blah" |
假设您定义了一个沿着
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | type Vehicle() = class end type Motorcycle(cyl : int) = inherit Vehicle() member this.Cylinders = cyl type Bicycle() = inherit Vehicle() type EngineType = Diesel | Gasoline type Car(engType : EngineType, doors : int) = inherit Vehicle() member this.EngineType = engType member this.Doors = doors |
我知道这是一个古老的话题,但在C 7中,你可以做到:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | switch(shape) { case Circle c: WriteLine($"circle with radius {c.Radius}"); break; case Rectangle s when (s.Length == s.Height): WriteLine($"{s.Length} x {s.Height} square"); break; case Rectangle r: WriteLine($"{r.Length} x {r.Height} rectangle"); break; default: WriteLine("<unknown shape>"); break; case null: throw new ArgumentNullException(nameof(shape)); } |
为了回答你的问题,是的,我认为模式匹配句法结构是有用的。我个人希望在C中看到对它的语法支持。
下面是我对一个类的实现,它提供(几乎)与您描述的相同的语法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | public class PatternMatcher<Output> { List<Tuple<Predicate<Object>, Func<Object, Output>>> cases = new List<Tuple<Predicate<object>,Func<object,Output>>>(); public PatternMatcher() { } public PatternMatcher<Output> Case(Predicate<Object> condition, Func<Object, Output> function) { cases.Add(new Tuple<Predicate<Object>, Func<Object, Output>>(condition, function)); return this; } public PatternMatcher<Output> Case<T>(Predicate<T> condition, Func<T, Output> function) { return Case( o => o is T && condition((T)o), o => function((T)o)); } public PatternMatcher<Output> Case<T>(Func<T, Output> function) { return Case( o => o is T, o => function((T)o)); } public PatternMatcher<Output> Case<T>(Predicate<T> condition, Output o) { return Case(condition, x => o); } public PatternMatcher<Output> Case<T>(Output o) { return Case<T>(x => o); } public PatternMatcher<Output> Default(Func<Object, Output> function) { return Case(o => true, function); } public PatternMatcher<Output> Default(Output o) { return Default(x => o); } public Output Match(Object o) { foreach (var tuple in cases) if (tuple.Item1(o)) return tuple.Item2(o); throw new Exception("Failed to match"); } } |
以下是一些测试代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | public enum EngineType { Diesel, Gasoline } public class Bicycle { public int Cylinders; } public class Car { public EngineType EngineType; public int Doors; } public class MotorCycle { public int Cylinders; } public void Run() { var getRentPrice = new PatternMatcher<int>() .Case<MotorCycle>(bike => 100 + bike.Cylinders * 10) .Case<Bicycle>(30) .Case<Car>(car => car.EngineType == EngineType.Diesel, car => 220 + car.Doors * 20) .Case<Car>(car => car.EngineType == EngineType.Gasoline, car => 200 + car.Doors * 20) .Default(0); var vehicles = new object[] { new Car { EngineType = EngineType.Diesel, Doors = 2 }, new Car { EngineType = EngineType.Diesel, Doors = 4 }, new Car { EngineType = EngineType.Gasoline, Doors = 3 }, new Car { EngineType = EngineType.Gasoline, Doors = 5 }, new Bicycle(), new MotorCycle { Cylinders = 2 }, new MotorCycle { Cylinders = 3 }, }; foreach (var v in vehicles) { Console.WriteLine("Vehicle of type {0} costs {1} to rent", v.GetType(), getRentPrice.Match(v)); } } |
模式匹配(如本文所述)的目的是根据类型规范解构值。但是,C中的类(或类型)的概念与您不一致。
多范式语言设计没有错,相反,使用C语言的lambdas非常好,haskell可以做一些必要的事情,例如io。但这不是一个非常优雅的解决方案,不是哈斯克尔的风格。
但是,由于顺序程序编程语言可以用lambda微积分来理解,而且C恰好适合顺序程序语言的参数,所以它是一种很好的适合。但是,从say haskell的纯功能上下文中提取一些东西,然后将该特性放入一种不纯粹的语言中,那么,仅仅这样做并不能保证更好的结果。
我的观点是,使模式匹配勾选的是语言设计和数据模型。尽管如此,我不认为模式匹配是C的一个有用特性,因为它不能解决典型的C问题,也不能很好地适应命令式编程范式。
我不知道这样做的方式是访客模式。您的访问者成员方法只是充当case结构,您可以让语言本身处理适当的分派,而不必"偷看"类型。
虽然打开类型不是很"c-sharpey",但我知道构造在一般使用中非常有用-我至少有一个个人项目可以使用它(尽管它是可管理的ATM)。在表达式树重新编写的情况下,是否存在许多编译性能问题?
我觉得这看起来很有趣(+1),但有一点要小心:C编译器非常擅长优化switch语句。不仅仅是短路-你会得到完全不同的IL,这取决于你有多少个案例等等。
您的具体示例确实做了一些我会发现非常有用的事情——没有与逐类型的大小写等价的语法,因为(例如)
这在动态应用程序中变得更有趣——这里的逻辑可以很容易地由数据驱动,从而实现"规则引擎"风格的执行。
你可以通过使用我写的一个图书馆,叫做
与
1 2 3 4 5 6 7 8 9 10 | OneOf<Motorcycle, Bicycle, Car> vehicle = ... //assign from one of those types var getRentPrice = vehicle .Match( bike => 100 + bike.Cylinders * 10, //"bike" here is typed as Motorcycle bike => 30, // returns a constant car => car.EngineType.Match( diesel => 220 + car.Doors * 20 petrol => 200 + car.Doors * 20 ) ); |
它在nuget上,目标是net451和netstandard1.6