在函数式编程风格中最好的任务是什么?

时间:2009-03-29 13:52:33

标签: c# f# c#-3.0 functional-programming

我刚刚发现了函数式编程风格,我相信它会减少开发工作,使代码更易于阅读,使软件更易于维护。然而,问题是我很难说服任何人。

好吧,最近我有机会就如何减少软件开发和维护工作发表演讲,我想向他们介绍函数式编程的概念以及它如何使团队受益。我有这样的想法,即向人们展示两组完全相同的代码,一个以非常强制的方式编码,另一个以非常实用的方式编写,以表明函数式编程可以使代码更简单,更容易理解和因此可维护。是否有这样的例子,除了Luca Bolognese的着名的正方形例子之外?

16 个答案:

答案 0 :(得分:64)

  

我刚刚发现了函数编程风格[...]   好吧,最近我有机会就如何减少问题发表演讲   软件开发的努力,我   想介绍一下这个概念   函数式编程。

如果您刚刚发现函数式编程,我建议尝试就该主题进行权威性发言。我知道前6个月我学习了F#,我的所有代码都只是C#,语法稍微笨拙。然而,在那段时间之后,我能够以惯用的功能风格编写一致的优秀代码。

我建议您也这样做:等待6个月左右,直到函数式编程风格更自然,然后进行演示。

  

我正在努力   说明功能的好处   编程,我有了想法   向人们展示2套代码   同样的事情,一个编码在一个非常   命令式的方式,另一种方式   非常实用的方式,以表明   函数式编程可以编写代码   方式更短,更容易理解和   因此保持。有这样的例子,   在着名的广场旁边   例如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 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#中简明扼要地编写这段代码。

答案 1 :(得分:14)

有很多例子,但没有一个像使用与你工作的某个项目相关的样本那样充满影响力。像Luca的“Sum Of Squares”这样的例子很棒但是如果有人用它作为我们的代码库如何写得更好的证明我就不会相信。所有的例子都证明有些东西在功能上写得更好。您需要证明的是您的代码库在功能上更好地编写

我的建议是在代码库中选择一些流行的麻烦点和一些核心点,并以功能样式重写它们。如果你能证明一个更好的解决方案,那么赢得同事将会有很长的路要走。

答案 2 :(得分:9)

功能风格的任务?任何时候你有一个共同的编码模式,并希望减少它。不久之前,我写了一些关于使用C#的功能样式,同时确保它是实用的:Practical Functional 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 =&gt; x.Bla&gt; 0)”中的1行内容,而不是C#样式中的7行(创建临时列表,循环并添加删除项目,循环然后删除这几项)。

希望是,即使语法很奇怪,人们也会认识到“哇,这更简单”。如果他们可以放下“详细= =更具可读性”和“看起来令人困惑”,你就有机会。

祝你好运。

答案 3 :(得分:2)

本质上,功能范例对并行处理非常有效:

  

“我想要真正有趣的事情   你要注意,这里就是那么快   当你想到地图并减少为   每个人都可以使用的功能,以及   他们使用它们,你只需要得到它们   一个超级来写硬编码   在全局上运行map和reduce   大规模并行计算机阵列,   以及以前工作的所有旧代码   当你刚刚运行一个循环时很好   只有它的速度快了几十万倍   这意味着它可以用来解决   瞬间出现了巨大的问题。

     

Lemme重复一遍。通过抽象而去   循环的概念,你可以   以任何你想要的方式实现循环,   包括以某种方式实施它   这可以很好地扩展   硬件“。

     

http://www.joelonsoftware.com/items/2006/08/01.html

答案 4 :(得分:2)

为功能风格撰写的最佳宣传论文是John Hughes撰写的一篇名为Why Functional Programming Matters的论文。我建议你自己做一些例子,直到你达到可以令人信服地在论文中提出论点的阶段。

本文中的许多例子都是数字化的,并没有引起今天观众的共鸣。我给学生们的另一个现代练习是使用该论文中的想法将大型媒体文件打包到4.7GB DVD上进行备份。他们使用迈克尔·米森马赫的“泡泡搜索”算法生成替代包装,并使用这种算法和休斯的技术,很容易得到每张DVD(除了最后一张)99.9%。很甜蜜。

答案 5 :(得分:1)

另一个例子是Quicksort algorithm。它可以用像Haskell这样的函数式语言非常简单地描述:

qsort []     = []
qsort (x:xs) = qsort (filter (< x) xs) ++ [x] ++ qsort (filter (>= x) xs)

但需要在迭代语言中进行更多编码。在引用的网站上,您还可以找到许多其他语言比较的例子。

答案 6 :(得分:1)

为了实现您的目标,并将其传达给组织中的其他人,您需要证明您公司的业务是以更好的方式构建的。

如果它对您的业务领域完全无用,那么使用几种算法来演示函数式编程的功能是没有用的。因此,请使用一些现有代码并在功能上重写它。如果你能证明这一点,那就更好了,人们会听你的 - 你已经向他们展示了一个具体的,相关的例子。如果你不能,那么函数式编程可能不是你一直在寻找的解决方案。

答案 7 :(得分:1)

如果用'功能样式'表示使用'map','apply','reduce','filter',lambda函数和列表推导等概念,那么很明显代码必须处理使用“功能样式”编写时,列表上的操作几乎总是更简洁。但是,如果你将“功能风格”与命令式代码混合在一种大多数命令式语言中,那真的只是风格问题。

在python中,例如,您可以重新实现Haskell qsort crackmigg,如下所示:

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])

可以说更像是“pythonic”。

但这显然比这里的实施更简洁:

http://hetland.org/coding/python/quicksort.html

顺便提一下,在我学习Haskell之前,我是如何考虑实现它的。

功能版非常清晰 if 只有当你适应函数式编程时才能轻松地完成filter要做的事情。 -worn C ++程序员在没有考虑太多问题的情况下进行for循环。这显然是真正的问题所在:功能性“风格”编程是一种完全不同的心态。如果与你一起工作的人不习惯递归思考,并且不是那种兴奋的类型,不仅仅是一种新技术,而是另一种思考解决问题的方式,那么任何数量的代码比较都不是不会赢得他们。

答案 8 :(得分:0)

一个很好的例子就是使用现有的编程语言创建自己的编程语言,您必须使用Monads

使用F#,编写解析逻辑要比使用C#简单得多。

看一下这篇文章:Functional .NET - LINQ or Language Integrated Monads?

答案 9 :(得分:0)

涉及回溯搜索和简化GUI中撤消支持的算法是我在实践中使用功能样式的两个地方。

答案 10 :(得分:0)

通常在功能样式中最容易完成的任务的简单示例是将数据从一种形式转换为另一种形式。 “平方和”是数据转换的一个简单例子。 Luca在去年的PDC演讲中展示了如何使用这种数据转换来实现更实用,下载和分析股票报价。该演示在F#中完成,但概念相同,可以应用于C#或大多数其他编程语言。

http://channel9.msdn.com/pdc2008/TL11/

答案 11 :(得分:0)

向他们展示jQuery迭代DOM元素的方式:

$(".magic-divs").click(function(){
    // FYI, in this context, "this" will be the element clicked on.
    alert("somebody clicked on " + this.id);
    this.hide();

});

$(".magic-divs").show();

VS。大多数谷歌搜索“javascript element by classname”的结果如何:

var arrayOfElements = // this is filled with the elements somehow
for(var i=0,j=arrayOfElements.length; i<j; i++) {
   alert("now I get to add an onclick event somehow " + i);
}
// i dont even want to type the ugly for-loop stuff to hide every div...

功能编程在上面的日常用途中非常有用!

(注意:我不知道我的例子是否符合函数式编程的确切定义,但如果确实如此,那么函数式编程就很棒了)

答案 12 :(得分:0)

我最近想出了一个小技巧来制作lambdas,传递给我的扩展方法看起来更像F#ish。 在这里:

我想做的是:

3.Times(() => Console.WriteLine("Doin' it"));

现在可以轻松实现扩展方法:

    public static void Times(this int times, Action action)
    {
        Enumerable.Range(1, times).ToList().ForEach(index => action());
    }

我不喜欢的是我在这里指定了索引:ForEach(index => action())虽然从未使用过,但我将其替换为_并得到ForEach(_ => action())

这很好,但现在我有动力让我的调用代码看起来很相似

(我从来不喜欢lambda表达式开头的“()”),所以代替:3.Times(() => ...);我想要3.Times(_ => ...); 实现这一点的唯一方法是将一个假参数传递给扩展方法,该方法永远不会被使用,所以我修改它是这样的:

    public static void Times(this int times, Action<byte> action)
    {
        Enumerable.Range(1, times).ToList().ForEach(_ => action(byte.MinValue));
    }

这允许我这样称呼它:

3.Times(_ => Console.WriteLine("Doin' it"));

没有太大的变化,但我仍然很高兴,可以轻松地进行那么小的调整,同时去除“()”噪音使其更具可读性。

答案 13 :(得分:0)

并没有真正回答这个问题,但对于那些想要了解C#中的函数式编程的人来说,这是一个非常好的链接

http://blogs.msdn.com/b/ericwhite/archive/2006/10/04/fp-tutorial.aspx

答案 14 :(得分:-1)

  1. 演示如何编码不同的数组。 SQL中的区别非常简单,但在内存阵列上却很难。现在很容易用LINQ区分数组。

  2. 您可以解释他们将来会有parralel LINQ(PLINQ)。当您开始编写功能代码时,您可以更轻松地对应用程序进行分类。 Google广泛使用MapReduce。

  3. 向他们解释LINQ是一种操纵不同类型数据的查询语言。在内存中,在数据库中,excell,web服务,xml文件,JSON文件。它是某种通用SQL。然而,不喜欢SQL的人会不那么信服。

  4. 我不会谈论函数式编程,我会解释LINQ如何帮助开发人员。

答案 15 :(得分:-3)

有趣的是,没有人真正回答过这个问题:在“功能风格”中最好完成哪项任务?

程序/算法由两部分组成:逻辑控制和数据结构。我认为在功能样式中最好的任务是那些涉及列表或数组的行为,就像它们的行为类似列表(例如qsort)。函数式编程语言对列表有很好的支持并非巧合。

当数据结构偏离列表时,我认为使用函数式编程风格会变得有些“不自然”。