我最近才发现了函数式编程风格,我相信这种风格可以减少开发工作量,使代码更易读,使软件更易于维护。然而,问题是我不擅长说服别人。
最近,我有机会发表关于如何减少软件开发和维护工作的演讲,并想介绍函数式编程的概念以及它如何使团队受益。我想展示两组完成相同任务的代码,一组采用非常命令式的方式编写,另一组采用非常函数式的方式编写,以此来说明函数式编程可以使代码更短、更易理解,从而更易于维护。除了 Luca Bolognese 的著名平方和示例之外,是否还有其他的示例可用?
我最近才发现了函数式编程风格,我相信这种风格可以减少开发工作量,使代码更易读,使软件更易于维护。然而,问题是我不擅长说服别人。
最近,我有机会发表关于如何减少软件开发和维护工作的演讲,并想介绍函数式编程的概念以及它如何使团队受益。我想展示两组完成相同任务的代码,一组采用非常命令式的方式编写,另一组采用非常函数式的方式编写,以此来说明函数式编程可以使代码更短、更易理解,从而更易于维护。除了 Luca Bolognese 的著名平方和示例之外,是否还有其他的示例可用?
我最近才发现函数式编程风格[...] 最近,我有机会做一个关于如何减少软件开发工作量的演讲, 我想介绍一下函数式编程的概念。
如果你只是最近才发现函数式编程,我不建议试图在这个主题上表现得很有权威性。我知道在学习F#的头6个月里,我的所有代码都只是带有更加笨拙语法的C#。然而,在那段时间之后,我能够以符合习惯的函数式风格编写一致好的代码。
我建议你也这样做:等待6个月左右,直到函数式编程风格变得更加自然,然后再进行演讲。
我正在尝试阐述函数式编程的好处,我想展示人们两组做同样事情的代码,一组采用非常命令式的方式编码,另一组采用非常函数式的方式编码,以此展示函数式编程可以使代码更短、更易于理解,从而更容易维护。除了Luca Bolognese著名的平方和例子外,还有其他类似的例子吗?
我曾向我的.NET用户组做过一个F#的演讲,我的很多用户都对F#的模式匹配印象深刻。具体来说,我展示了如何在C#和F#中遍历抽象语法树:
using System;
namespace ConsoleApplication1
{
public interface IExprVisitor<t>
{
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<t>(IExprVisitor<t> 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<t>(IExprVisitor<t> visitor)
{
return visitor.Visit(this);
}
}
public class And : BinExpr
{
public And(Expr first, Expr second) : base(first, second) { }
public override t Accept<t>(IExprVisitor<t> visitor)
{
return visitor.Visit(this);
}
}
public class Nand : BinExpr
{
public Nand(Expr first, Expr second) : base(first, second) { }
public override t Accept<t>(IExprVisitor<t> visitor)
{
return visitor.Visit(this);
}
}
public class Or : BinExpr
{
public Or(Expr first, Expr second) : base(first, second) { }
public override t Accept<t>(IExprVisitor<t> visitor)
{
return visitor.Visit(this);
}
}
public class Xor : BinExpr
{
public Xor(Expr first, Expr second) : base(first, second) { }
public override t Accept<t>(IExprVisitor<t> visitor)
{
return visitor.Visit(this);
}
}
public class Not : UnaryOp
{
public Not(Expr first) : base(first) { }
public override t Accept<t>(IExprVisitor<t> visitor)
{
return visitor.Visit(this);
}
}
public class EvalVisitor : IExprVisitor<bool>
{
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<string>
{
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行代码。
这是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行。由于它使用模式匹配而不是访问者模式,我们不会失去任何类型安全性,并且代码非常易于阅读。
任何类型的符号处理在F#中编写比C#容易几个数量级。
[编辑以添加:] 哦,而模式匹配不仅是访问者模式的替代,还允许您针对数据的形状进行匹配。例如,这是一个将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#中根本无法简洁地编写此代码。
let rec loop = match loop with
- user522860let rec loop x = match x with...
吗? - Mauricio SchefferExpr
子类型的实现,您也会在编译时出现错误,因为 Expr
知道 IExprVisitor
,而 IExprVisitor
知道每种可能的 Expr
类型。如果您使用 switch/case
实现它,很容易忘记专门为新类型进行操作。 - sunside有许多示例,但没有哪个能像使用与你工作项目相关的示例那样具有影响力。像Luca的 "平方和" 这样的示例非常棒,但如果有人将其用作证明我们的代码库可以写得更好的证据,我是不会被说服的。所有的例子都只证明了有些东西在函数式编程中写得更好。你需要证明的是你的代码库在函数式编程中写得更好。
我的建议是选择一些常见的问题和代码库中的核心部分,并以函数式风格重写它们。如果您可以展示出明显更好的解决方案,这将有助于赢得同事的信任。
最好的函数式编程倡导论文是由约翰·休斯(John Hughes)撰写的一篇名为《为什么函数式编程很重要》的论文。我建议你自己做一些例子,直到你能够有说服力地阐述该论文中所阐述的观点。
该论文中的许多示例都是关于数字的,与今天的读者不太相关。我给我的学生们提供了一个更具时代感的练习,即使用该论文中的思想将大型媒体文件打包成4.7GB的DVD备份。他们使用迈克尔·米茨恩马赫(Michael Mitzenmacher)的“冒泡搜索”算法生成备选方案,并使用这个算法和休斯的技术,很容易使每个DVD(除了最后一个)达到99.9%的容量利用率。非常棒。
qsort [] = []
qsort (x:xs) = qsort (filter (< x) xs) ++ [x] ++ qsort (filter (>= x) xs)
但需要在迭代语言中进行更多编码。在引用的网站上,您还可以找到许多其他语言之间的比较示例。
def qsort(list):
if list == []:
return []
else:
x = list[0]; xs = list[1:]
return qsort(filter(lambda y: y<x, xs)) + [x] + qsort(filter(lambda y: y>= x, xs))
虽然将最后一行写成
return qsort([y for y in xs if y<x]) + [x] + qsort([y for y in xs if y>=x])
可以说更符合Python的风格。
但这显然比此处的实现更简洁:
http://hetland.org/coding/python/quicksort.html
顺便说一下,在我学习 Haskell 之前,这是我想要实现它的方式。
如果你已经适应了函数式编程并像老练的 C++ 程序员一样轻松理解 filter
的作用,那么函数式版本就非常清晰。这显然是实际问题的关键:函数“风格”编程是一种完全不同的思维方式。如果你合作的人不习惯递归思考,并且不是那种对于解决问题有全新思路的方法感到兴奋的类型,那么任何数量的代码比较都无法说服他们。
回溯搜索算法和简化GUI中的撤销支持是我在实践中使用函数式风格的两个地方。