我刚刚发现了函数编程风格,我相信它会减少开发工作,使代码更容易阅读,使软件更易于维护.然而,问题是我在说服任何人.
好吧,最近我有机会就如何减少软件开发和维护工作发表演讲,我想向他们介绍函数式编程的概念以及它如何使团队受益.我有这样的想法,即向人们展示两组完全相同的代码,一个以非常强制的方式编码,另一个以非常实用的方式编写,以表明函数式编程可以使代码更简单,更容易理解和因此可维护.是否有这样的例子,除了Luca Bolognese的着名的平方和例子?
我刚刚发现了函数式编程风格[...]嗯,最近我有机会就如何减少软件开发工作发表演讲,我想介绍函数式编程的概念.
如果你刚刚发现函数式编程,我不建议尝试在这个主题上权威地说话.我知道前6个月我学习了F#,我的所有代码都只是C#,语法稍微笨拙.然而,在那段时间之后,我能够以惯用的功能风格编写一致的优秀代码.
我建议你这样做:等待6个月左右,直到函数式编程风格更自然地出现,然后给你的演示文稿.
我试图说明函数式编程的好处,我有想法向人们展示两组代码执行相同的操作,一组以非常强制的方式编码,另一组以非常实用的方式编写,以显示函数式编程可以使代码更简单,更易于理解,从而维护.是否有这样的例子,除了卢卡博洛尼塞的着名的平方和例子?
我向我所在地区的.NET用户组发了一个F#演示文稿,我小组中的很多人都对F#的模式匹配印象深刻.具体来说,我展示了如何遍历C#和F#中的抽象语法树:
using System; namespace ConsoleApplication1 { public interface IExprVisitor{ t Visit(TrueExpr expr); t Visit(And expr); t Visit(Nand expr); t Visit(Or expr); t Visit(Xor expr); t Visit(Not expr); } public abstract class Expr { public abstract t Accept (IExprVisitor visitor); } public abstract class UnaryOp : Expr { public Expr First { get; private set; } public UnaryOp(Expr first) { this.First = first; } } public abstract class BinExpr : Expr { public Expr First { get; private set; } public Expr Second { get; private set; } public BinExpr(Expr first, Expr second) { this.First = first; this.Second = second; } } public class TrueExpr : Expr { public override t Accept (IExprVisitor visitor) { return visitor.Visit(this); } } public class And : BinExpr { public And(Expr first, Expr second) : base(first, second) { } public override t Accept (IExprVisitor visitor) { return visitor.Visit(this); } } public class Nand : BinExpr { public Nand(Expr first, Expr second) : base(first, second) { } public override t Accept (IExprVisitor visitor) { return visitor.Visit(this); } } public class Or : BinExpr { public Or(Expr first, Expr second) : base(first, second) { } public override t Accept (IExprVisitor visitor) { return visitor.Visit(this); } } public class Xor : BinExpr { public Xor(Expr first, Expr second) : base(first, second) { } public override t Accept (IExprVisitor visitor) { return visitor.Visit(this); } } public class Not : UnaryOp { public Not(Expr first) : base(first) { } public override t Accept (IExprVisitor visitor) { return visitor.Visit(this); } } public class EvalVisitor : IExprVisitor { public bool Visit(TrueExpr expr) { return true; } public bool Visit(And expr) { return Eval(expr.First) && Eval(expr.Second); } public bool Visit(Nand expr) { return !(Eval(expr.First) && Eval(expr.Second)); } public bool Visit(Or expr) { return Eval(expr.First) || Eval(expr.Second); } public bool Visit(Xor expr) { return Eval(expr.First) ^ Eval(expr.Second); } public bool Visit(Not expr) { return !Eval(expr.First); } public bool Eval(Expr expr) { return expr.Accept(this); } } public class PrettyPrintVisitor : IExprVisitor { public string Visit(TrueExpr expr) { return "True"; } public string Visit(And expr) { return string.Format("({0}) AND ({1})", expr.First.Accept(this), expr.Second.Accept(this)); } public string Visit(Nand expr) { return string.Format("({0}) NAND ({1})", expr.First.Accept(this), expr.Second.Accept(this)); } public string Visit(Or expr) { return string.Format("({0}) OR ({1})", expr.First.Accept(this), expr.Second.Accept(this)); } public string Visit(Xor expr) { return string.Format("({0}) XOR ({1})", expr.First.Accept(this), expr.Second.Accept(this)); } public string Visit(Not expr) { return string.Format("Not ({0})", expr.First.Accept(this)); } public string Pretty(Expr expr) { return expr.Accept(this).Replace("(True)", "True"); } } class Program { static void TestLogicalEquivalence(Expr first, Expr second) { var prettyPrinter = new PrettyPrintVisitor(); var eval = new EvalVisitor(); var evalFirst = eval.Eval(first); var evalSecond = eval.Eval(second); Console.WriteLine("Testing expressions:"); Console.WriteLine(" First = {0}", prettyPrinter.Pretty(first)); Console.WriteLine(" Eval(First): {0}", evalFirst); Console.WriteLine(" Second = {0}", prettyPrinter.Pretty(second)); Console.WriteLine(" Eval(Second): {0}", evalSecond);; Console.WriteLine(" Equivalent? {0}", evalFirst == evalSecond); Console.WriteLine(); } static void Main(string[] args) { var P = new TrueExpr(); var Q = new Not(new TrueExpr()); TestLogicalEquivalence(P, Q); TestLogicalEquivalence( new Not(P), new Nand(P, P)); TestLogicalEquivalence( new And(P, Q), new Nand(new Nand(P, Q), new Nand(P, Q))); TestLogicalEquivalence( new Or(P, Q), new Nand(new Nand(P, P), new Nand(Q, Q))); TestLogicalEquivalence( new Xor(P, Q), new Nand( new Nand(P, new Nand(P, Q)), new Nand(Q, new Nand(P, Q))) ); Console.ReadKey(true); } } }
上面的代码是用惯用的C#风格编写的.它使用访客模式而不是类型测试来保证类型安全.这大概是218 LOC.
这是F#版本:
#light open System type expr = | True | And of expr * expr | Nand of expr * expr | Or of expr * expr | Xor of expr * expr | Not of expr let (^^) p q = not(p && q) && (p || q) // makeshift xor operator let rec eval = function | True -> true | And(e1, e2) -> eval(e1) && eval(e2) | Nand(e1, e2) -> not(eval(e1) && eval(e2)) | Or(e1, e2) -> eval(e1) || eval(e2) | Xor(e1, e2) -> eval(e1) ^^ eval(e2) | Not(e1) -> not(eval(e1)) let rec prettyPrint e = let rec loop = function | True -> "True" | And(e1, e2) -> sprintf "(%s) AND (%s)" (loop e1) (loop e2) | Nand(e1, e2) -> sprintf "(%s) NAND (%s)" (loop e1) (loop e2) | Or(e1, e2) -> sprintf "(%s) OR (%s)" (loop e1) (loop e2) | Xor(e1, e2) -> sprintf "(%s) XOR (%s)" (loop e1) (loop e2) | Not(e1) -> sprintf "NOT (%s)" (loop e1) (loop e).Replace("(True)", "True") let testLogicalEquivalence e1 e2 = let eval1, eval2 = eval e1, eval e2 printfn "Testing expressions:" printfn " First = %s" (prettyPrint e1) printfn " eval(e1): %b" eval1 printfn " Second = %s" (prettyPrint e2) printfn " eval(e2): %b" eval2 printfn " Equilalent? %b" (eval1 = eval2) printfn "" let p, q = True, Not True let tests = [ p, q; Not(p), Nand(p, p); And(p, q), Nand(Nand(p, q), Nand(p, q)); Or(p, q), Nand(Nand(p, p), Nand(q, q)); Xor(p, q), Nand( Nand(p, Nand(p, q)), Nand(q, Nand(p, q)) ) ] tests |> Seq.iter (fun (e1, e2) -> testLogicalEquivalence e1 e2) Console.WriteLine("(press any key)") Console.ReadKey(true) |> ignore
这是65 LOC.由于它使用模式匹配而不是访问者模式,因此我们不会丢失任何类型安全性,并且代码非常易于阅读.
任何类型的符号处理都比F#更容易用F#写入.
[编辑添加:]哦,模式匹配不仅仅是访问者模式的替代,它还允许您匹配数据的形状.例如,这是一个将Nand转换为等价的函数:
let rec simplify = function | Nand(p, q) when p = q -> Not(simplify p) | Nand(Nand(p1, q1), Nand(p2, q2)) when equivalent [p1; p2] && equivalent [q1; q2] -> And(simplify p1, simplify q1) | Nand(Nand(p1, p2), Nand(q1, q2)) when equivalent [p1; p2] && equivalent [q1; q2] -> Or(simplify p1, simplify q1) | Nand(Nand(p1, Nand(p2, q1)), Nand(q2, Nand(p3, q3))) when equivalent [p1; p2; p3] && equivalent [q1; q2; q3] -> Xor(simplify p1, simplify q1) | Nand(p, q) -> Nand(simplify p, simplify q) | True -> True | And(p, q) -> And(simplify p, simplify q) | Or(p, q) -> Or(simplify p, simplify q) | Xor(p, q) -> Xor(simplify p, simplify q) | Not(Not p) -> simplify p | Not(p) -> Not(simplify p)
它不可能在C#中简明扼要地编写这段代码.
有很多例子,但没有一个像使用与你工作的一个项目相关的样本一样充满影响力.像Luca的"Sum Of Squares"这样的例子很棒但是如果有人用它作为我们的代码库如何写得更好的证明我就不会相信.所有的例子都证明有些东西在功能上写得更好.您需要证明的是您的代码库在功能上更好地编写
我的建议是选择一些流行的麻烦点和代码库中的一些核心点,并以功能样式重写它们.如果您能够展示出更好的解决方案,那么赢得同事将会有很长的路要走.
功能风格的任务?任何时候你有一个共同的编码模式,并希望减少它.不久之前,我写了一些关于使用C#的功能样式,同时确保它是实用的:实用功能C#(我很想在这里链接到我自己的东西,但我认为在这种情况下它是相关的).如果您有一个共同的"企业"应用程序,那么显示表达式在模式匹配中的可爱程度将不会太令人信服.
但在现实世界的应用程序中,有很多模式会以较低的编码级别出现.使用更高阶的函数,你可以让它们消失.正如我在那篇博文中所展示的那样,我最喜欢的例子是WCF的"try-close/finally-abort"模式."try/finally-dispose"模式非常常见,它变成了一个语言关键字:using."锁定"也是一样.这些都被简单地表示为高阶函数,并且仅仅因为C#最初不支持它们,我们需要硬编码语言关键字来支持它们.(快速:切换你的"锁定"块以使用ReaderWriter锁.糟糕,我们必须先写一个更高阶的功能.)
但也许令人信服只需要看微软.泛型又是参数多态?这不是OO,而是一个很好的功能概念,现在,每个人都喜欢.如果没有它,可爱的Ninject框架将无法运行.Lambda表达式?作为表达树,他们是如何LINQ,Fluent NHibernate等获得他们所有的力量.同样,这不是来自OO或命令式编程.新的线程库?非常丑陋没有封闭.
因此,在过去的十年中,函数式编程一直在为.NET之类的东西带来祝福.主要的进步(例如泛型,"LINQ")直接来自函数式语言.为什么不意识到它有什么东西并且更多地参与其中呢?这就是我对怀疑论者的看法.
更大的问题实际上是让人们对更高阶函数的理解有所突破.虽然它很容易,如果你以前从未见过它,那可能会令人难以理解.(哎呀,似乎很多人认为泛型只是用于类型安全的集合,而LINQ只是嵌入式SQL.)
所以,你应该做的就是浏览你的代码库,找到一个过于复杂的命令式噩梦.搜索底层模式,并使用函数将它们很好地串在一起.如果你找不到任何东西,你可能会满足于只是演示关闭列表.例如,"在此列表中找到所有Foos并将其删除".这是功能样式"myList.Remove(x => x.Bla> 0)"中的1行内容,而不是C#样式中的7行(创建临时列表,循环并添加删除项目,循环并删除项目).
希望是,即使语法很奇怪,人们也会认识到"哇,这更简单".如果他们可以放下"详细= =更具可读性"和"看起来令人困惑",你就有机会.
祝好运.