【问题标题】:Explaining functional programming to object-oriented programmers and less technical people向面向对象的程序员和技术含量较低的人解释函数式编程
【发布时间】:2010-02-19 04:24:13
【问题描述】:

有哪些很好的例子可以用来解释函数式编程?

受众可能是编程经验很少的人,或者只有面向对象经验的人。

【问题讨论】:

  • 非程序员是技术领域的专业人士还是外行?我们是在谈论《科学美国人》还是《大众科学》?
  • @outis 编程专业人士或与编程专业人士一起工作的人,例如经理和前程序员。
  • 你想解释一下什么是 FP,如何使用它,它的好处还是这些的组合?
  • 我主要是想帮助人们了解它是什么以及它与其他范式(例如 OO)有何不同。

标签: oop functional-programming


【解决方案1】:

观众应该是有 一点编程经验,

真的可以向没有太多编程经验的人解释函数式编程吗?

或只有 面向对象的体验。

也许最好的例子是将已知的设计模式转换成它们的等效功能。让我们以将整数列表转换为字符串列表的规范示例为例:

using System;

namespace Juliet
{
    interface IConvertor<T, U>
    {
        U Convert(T value);
    }

    class Program
    {
        static U[] Convert<T, U>(T[] input, IConvertor<T, U> convertor)
        {
            U[] res = new U[input.Length];
            for (int i = 0; i < input.Length; i++)
            {
                res[i] = convertor.Convert(input[i]);
            }
            return res;
        }

        class SquareInt : IConvertor<int, string>
        {
            public string Convert(int i)
            {
                return (i * i).ToString();
            }
        }

        class ScaleInt : IConvertor<int, string>
        {
            readonly int Scale;

            public ScaleInt(int scale)
            {
                this.Scale = scale;
            }

            public string Convert(int i)
            {
                return (i * Scale).ToString();
            }
        }

        static void Main(string[] args)
        {
            int[] nums = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

            string[] squared = Convert<int, string>(nums, new SquareInt());
            string[] tripled = Convert<int, string>(nums, new ScaleInt(3));
        }
    }
}

它简单、易读、面向对象,甚至是通用的,所以它适用于任意类型,那么有什么问题呢?首先,它很臃肿:我有一个接口定义和两个接口实现。如果我需要另一个转换怎么办?好吧,我需要另一个接口实现——它很快就会失控。

仔细想想,IConvertor&lt;T, U&gt; 类只是一个名为Convert 的函数的包装器——该类的存在实际上是为了帮助我们将Convert 传递给其他函数。如果你能理解这么多,那么你已经理解了函数作为一等值背后的基本原理——函数式编程就是将函数传递给其他函数,就像传递一个人、一个 int 或一个字符串一样。

人们通常更喜欢函数式编程,因为它可以帮助他们避免单一方法的接口和实现。我们不传递类,而是通过名称或匿名传递函数:

using System;

namespace Juliet
{
    class Program
    {
        static U[] Convert<T, U>(T[] input, Func<T, U> convertor)
        {
            U[] res = new U[input.Length];
            for (int i = 0; i < input.Length; i++)
            {
                res[i] = convertor(input[i]);
            }
            return res;
        }

        static string SquareInt(int i)
        {
            return (i * i).ToString();
        }

        static void Main(string[] args)
        {
            int[] nums = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

            string[] squared = Convert<int, string>(nums, SquareInt); // pass function by name
            string[] tripled = Convert<int, string>(nums, i => (i * 3).ToString()); // or pass anonymously
        }
    }
}

好的,所以现在我们用更少的代码行拥有完全相同的程序,它的可读性和它的作用非常明显。

现在有人可能会说“这是一个巧妙的技巧,但我什么时候可以使用它”——在很多情况下,您都希望像这样传递函数。它为您提供了更多以新颖的方式抽象程序控制流的能力。改编自here 所示的示例,考虑一个处理文件的类:

class FileFunctions
{
    internal void SaveFile()
    {
        SaveFileDialog fileDialog = new SaveFileDialog();
        fileDialog.Filter = "Text files (*.txt)|*.txt|All files (*.*)|*.*";
        if (fileDialog.ShowDialog() == DialogResult.OK)
        {
            File.AppendAllText(fileDialog.FileName, MyDocument.Data);
        }
    }

    internal void WriteFile()
    {
        OpenFileDialog fileDialog = new OpenFileDialog();
        fileDialog.Filter = "Text files (*.txt)|*.txt|All files (*.*)|*.*";
        if (fileDialog.ShowDialog() == DialogResult.OK)
        {
            MyDocument.Data = File.ReadAllText(fileDialog.FileName);
        }
    }
}

哎呀。代码是重复的,但它在不同的类上执行操作并调用不同的方法。您将如何抽象出 OO 世界中的重复代码?在函数式编程中,这是微不足道的:

class FileFunctions
{
    internal void ShowDialogThen(FileDialog dialog, Action<FileDialog> f)
    {
        dialog.Filter = "Text files (*.txt)|*.txt|All files (*.*)|*.*";
        if (dialog.ShowDialog() == DialogResult.OK)
        {
            f(dialog);
        }
    }

    internal void SaveFile()
    {
        ShowDialogThen(
            new SaveFileDialog(),
            dialog => File.AppendAllText(dialog.FileName, MyDocument.Data));
    }

    internal void WriteFile()
    {
        ShowDialogThen(
            new OpenFileDialog(),
            dialog => { MyDocument.Data = File.ReadAllText(dialog.FileName); });
    }
}

太棒了。

【讨论】:

  • 我喜欢这个主意。我认为展示一个面向功能的 OO 示例可能是解释它的最佳方式。
【解决方案2】:

首先通过其基本特征来描述函数式编程可能更容易:

  1. 函数(显然)
  2. 没有副作用
  3. 不变性
  4. 递归很重要
  5. 非常可并行化

其中,最重要的特征是没有副作用的概念,其他特征由此而来。

函数式编程用于您可能意想不到的地方。例如,它用于运行交通信号灯和通信交换机(爱立信交换机运行 Erlang)。

【讨论】:

    【解决方案3】:

    没有多少编程经验的人可能更容易,因为他们不会有太多的编程概念会妨碍他们:函数式编程与表达式等数学函数非常相似,所以向他们展示一些数学知识。

    对于 OO 人群,您可以展示闭包如何类似于封装,以及函数式编程如何包含 OOP。您还可以展示高阶函数是多么有用,特别是它们如何导致一些 OOP 模式(例如策略和模板模式,以及具有多方法的语言中的访问者模式)逐渐消失在语言中(Peter Norvig asserts 那 16 个23 GOF patterns are simpler in Dylan and Lisp)。作为对应物,基于消息传递的 OOP(而不是 CLOS 方法)是 FP 中的一种模式,在 OOP 中是不可见的。

    展示将通用算术表达式以及函数组合和微分编写为编程结构是多么容易,并且您已经掌握了它们。

    你可能想也可能不想向 OO 人展示 CLOS 的多种方法;他们要么暴动,要么疯狂。无论哪种方式,它都可能很混乱。

    展示 FP 有多灵活的其他一些可能性:可以使用无效匿名函数来实现惰性求值。 FP 可以支持基于类和基于原型的 OOP。还有很多其他示例(例如延续传递样式),但它们通常需要在学习之前熟悉 FP。

    SICP 有很多有趣的例子和问题;它应该被证明是鼓舞人心的。

    【讨论】:

      【解决方案4】:

      尝试查看这篇名为 Functional Programming for the Rest of Us 的文章,以下是摘录。

      在本文中,我将使用用 Java 编写的示例解释函数式语言中最广泛使用的想法(是的,如果你觉得特别受虐,你可以用 Java 编写函数式程序)。在接下来的几节中,我们将按原样使用 Java,并将对其进行修改以将其转换为可用的函数式语言。让我们开始我们的探索吧。

      【讨论】:

        【解决方案5】:

        OO 是关于名词,而函数式编程是关于动词。阅读Steve Yegge 撰写的关于该主题的相当不错的博客。

        【讨论】:

        • 其实这完全是误导。如果您采用 FP 范式的理想,那么它是 declarative,并且没有任何动词。同样,在 OO 中,编程仍然是程序性的,因此可以说都是关于动词的。也就是说,它正好反过来。 AFAIK,Steve Yegge 不是函数式程序员,所以不要相信他的话。
        • @AndreasRossberg 我不相信任何人的话 :-) 但这是一本好书。
        【解决方案6】:

        面向对象程序员的函数式编程,作者:Brian Marick。我没有关联,或者什么,只是现在来了:

        https://leanpub.com/fp-oo

        鲍勃叔叔喜欢它! :)

        【讨论】:

        • 感谢您的建议,看起来很有趣。
        【解决方案7】:

        Why Functional Programming Matters by John Hughes 有一些很好的例子,但更好的是,它解释了函数式编程语言的为什么,而不仅仅是如何。你可以做的比基于那篇论文做一个演示更糟糕!

        【讨论】:

          【解决方案8】:

          不可变数据结构

          您知道,我之前曾向人们评论过函数式编程,并提出了整个“不可变数据结构”的东西。这通常会让人们大吃一惊——如果你不能真正添加​​一些东西,你应该如何将它添加到集合中?

          直接的回答是“你没有,你只是创建了一个添加了对象的数据结构的新版本”,而直接的回答是“听起来效率不高”。原来如此。

          不可变堆栈

          规范的不可变数据结构是一个不可变堆栈,它基本上是一个包含两个只读值的节点:一个头和一个尾。从堆栈中推入和弹出是 O(1)。在列表中间进行索引查找、插入/删除需要 O(n) 时间,顺便说一下,这与可变堆栈相同。

          using System;
          
          namespace Juliet
          {
              public abstract class Stack<T>
              {
                  public static readonly Stack<T> Empty = new Nil();
          
                  public abstract T Head { get; }
                  public abstract Stack<T> Tail { get; }
                  public abstract bool IsEmpty { get; }
                  public Stack<T> Push(T value) { return new Cons(value, this); }
                  public Stack<T> Append(Stack<T> other)
                  {
                      if (this.IsEmpty) { return other; }
                      else { return new Cons(this.Head, this.Tail.Append(other)); }
                  }
          
                  sealed class Nil : Stack<T>
                  {
                      public override T Head { get { throw new Exception("Empty stack"); } }
                      public override Stack<T> Tail { get { throw new Exception("Empty stack"); } }
                      public override bool IsEmpty { get { return true; } }
                  }
          
                  sealed class Cons : Stack<T>
                  {
                      private readonly T _head;
                      private readonly Stack<T> _tail;
          
                      public override T Head { get { return _head; ; } }
                      public override Stack<T> Tail { get { return _tail; } }
                      public override bool IsEmpty { get { return false; } }
          
                      public Cons(T head, Stack<T> tail)
                      {
                          _head = head;
                          _tail = tail;
                      }
                  }
              }
          
              class Program
              {       
                  static void Main(string[] args)
                  {
                      var s = Stack<int>.Empty.Push(1).Push(2).Push(3).Push(4).Push(5);
                      while (!s.IsEmpty)
                      {
                          Console.Write("{0} ", s.Head);
                          s = s.Tail;
                      }
                      Console.ReadKey(true);
                  }
              }
          }
          

          不可变的 AVL 树

          大多数人对堆栈印象不深,因为它不是一种“严肃”的数据结构,但 AVL 树和其他自平衡数据结构令人印象深刻。树实际上很容易写成不可变的数据结构。

          这个版本支持 O(log n) 的插入和查找,没有显示删除,但它也可以在 O(log n) 中实现。换句话说,不可变 AVL 树的计算复杂度与其可变变体相同:

          using System;
          using System.Linq;
          
          namespace Juliet
          {
              public abstract class AvlTree<T>
                  where T : IComparable<T>
              {
                  static AvlTree<T> Make(AvlTree<T> left, T value, AvlTree<T> right)
                  {
                      return new Node(left, value, right, Math.Max(left.Height, right.Height) + 1, left.Count + right.Count + 1);
                  }
          
                  public static readonly AvlTree<T> Empty = new Nil();
          
                  protected abstract AvlTree<T> Left { get; }
                  protected abstract AvlTree<T> Right { get; }
                  protected abstract T Value { get; }
                  public abstract int Height { get; }
                  public abstract int Count { get; }
          
                  public bool Contains(T v)
                  {
                      if (this.Height == 0) { return false; }
                      else
                      {
                          int compare = v.CompareTo(this.Value);
                          if (compare < 0) { return this.Left.Contains(v); }
                          else if (compare > 0) { return this.Right.Contains(v); }
                          else { return true; }
                      }
                  }
          
                  public AvlTree<T> Insert(T v)
                  {
                      if (this.Height == 0) { return Make(this, v, this); }
                      else
                      {
                          int compare = v.CompareTo(this.Value);
                          if (compare < 0) { return Balance(Make(this.Left.Insert(v), this.Value, this.Right)); }
                          else if (compare > 0) { return Balance(Make(this.Left, this.Value, this.Right.Insert(v))); }
                          else { return this; }
                      }
                  }
          
                  private static AvlTree<T> Balance(AvlTree<T> tree)
                  {
                      if (tree.Height > 0)
                      {
                          int outerBalanceFactor = tree.Left.Height - tree.Right.Height;
                          if (outerBalanceFactor <= -2 && tree.Right.Height > 0) // right-side too heavy
                          {
                              int innerBalanceFactor = tree.Right.Left.Height - tree.Right.Right.Height;
                              if (innerBalanceFactor <= -1) // right-right case
                              {
                                  return LeftRotate(tree);
                              }
                              else if (innerBalanceFactor >= 1) // right-left case
                              {
                                  return DoubleLeftRotate(tree);
                              }
                          }
                          else if (outerBalanceFactor >= 2 && tree.Left.Height > 0) // left-side too heavy
                          {
                              int innerBalanceFactor = tree.Left.Left.Height - tree.Left.Right.Height;
                              if (innerBalanceFactor <= -1) // left-left case
                              {
                                  return DoubleRightRotate(tree);
                              }
                              else if (innerBalanceFactor >= 1) // left-right case
                              {
                                  return RightRotate(tree);
                              }
                          }
                      }
                      return tree;
                  }
          
                  private static AvlTree<T> LeftRotate(AvlTree<T> tree)
                  {
                      if (tree.Height > 0 && tree.Right.Height > 0)
                      {
                          T p = tree.Value;
                          T q = tree.Right.Value;
                          AvlTree<T> a = tree.Left;
                          AvlTree<T> b = tree.Right.Left;
                          AvlTree<T> c = tree.Right.Right;
                          return Make(Make(a, p, b), q, c);
                      }
                      return tree;
                  }
          
                  private static AvlTree<T> RightRotate(AvlTree<T> tree)
                  {
                      if (tree.Height > 0 && tree.Left.Height > 0)
                      {
                          T q = tree.Value;
                          T p = tree.Left.Value;
                          AvlTree<T> a = tree.Left.Left;
                          AvlTree<T> b = tree.Left.Right;
                          AvlTree<T> c = tree.Right;
                          return Make(a, p, Make(b, q, c));
                      }
                      return tree;
                  }
          
                  private static AvlTree<T> DoubleLeftRotate(AvlTree<T> tree)
                  {
                      if (tree.Height > 0)
                      {
                          // right-rotate right child, left-rotate root
                          return LeftRotate(Make(tree.Left, tree.Value, RightRotate(tree.Right)));
                      }
                      return tree;
                  }
          
                  private static AvlTree<T> DoubleRightRotate(AvlTree<T> tree)
                  {
                      if (tree.Height > 0)
                      {
                          // left-rotate left child, right-rotate root
                          return RightRotate(Make(LeftRotate(tree.Left), tree.Value, tree.Right));
                      }
                      return tree;
                  }
          
          
                  sealed class Nil : AvlTree<T>
                  {
                      protected override AvlTree<T> Left { get { throw new Exception("Empty tree"); } }
                      protected override AvlTree<T> Right { get { throw new Exception("Empty tree"); } }
                      protected override T Value { get { throw new Exception("Empty tree"); } }
                      public override int Height { get { return 0; } }
                      public override int Count { get { return 0; } }
                  }
          
                  sealed class Node : AvlTree<T>
                  {
                      readonly AvlTree<T> _left;
                      readonly AvlTree<T> _right;
                      readonly T _value;
                      readonly int _height;
                      readonly int _count;
          
                      protected override AvlTree<T> Left { get { return _left; } }
                      protected override AvlTree<T> Right { get { return _right; } }
                      protected override T Value { get { return _value; } }
                      public override int Height { get { return _height; } }
                      public override int Count { get { return _count; } }
          
                      public Node(AvlTree<T> left, T value, AvlTree<T> right, int height, int count)
                      {
                          _left = left;
                          _right = right;
                          _value = value;
                          _height = height;
                          _count = count;
                      }
                  }
              }
          
              class Program
              {       
                  static void Main(string[] args)
                  {
                      var tree = AvlTree<int>.Empty;
                      foreach(int i in Enumerable.Range(1, 1000000).OrderBy(_ => Guid.NewGuid()))
                      {
                          tree = tree.Insert(i);
                      }
          
                      Console.Write("Tree height: {0}, tree count: {1}", tree.Height, tree.Count);
          
                      Console.ReadKey(true);
                  }
              }
          }
          

          【讨论】:

            【解决方案9】:

            坦率地说,我认为你做不到。函数式编程(如“好的”面向对象编程,与“带有类的过程”)需要心理模型转变。如果不真正花时间并通过使用它,您将无法将其理解为程序编码器。

            如果你真的想让某人理解函数式编程,给他们买一本 Little Schemer 或其他类似的书,让他们坐下来完成练习。没有其他东西可以帮助他们建立必要的心理模型。

            【讨论】:

              【解决方案10】:

              我们在 Haskell 中使用了本教程,效果非常好,它轻松愉快,但在解释方面做得很好:

              Learn you a Haskell for Great Good

              【讨论】:

                【解决方案11】:

                将其与句子结构进行比较。

                我击球、接球、投球:
                程序
                击球(球)
                接球(球)
                投掷(球)

                OO
                Ball.Hit()
                Ball.Catch()
                Ball.Throw()

                【讨论】:

                • person.hit(Bat b, IThwackable toBeThwacked), person.getGlove().catch(ICatchable toBeCaught), person.getArm(ArmPositionFactory.getPostision(new ArmPosition(PositionConstants.Extended | PositionConstants.Fast) ).throw(IThrowable throwable)
                【解决方案12】:

                新答案。这适用于非编程高级用户和 OO 程序员。

                指出 Excel 是功能性的。

                电子表格中的每个单元格都是一个变量,其中包含一个声明其他单元格/变量之间关系的函数。

                【讨论】:

                  猜你喜欢
                  • 2010-12-27
                  • 1970-01-01
                  • 2015-08-20
                  • 2010-11-04
                  • 2016-08-11
                  • 2018-08-02
                  • 1970-01-01
                  • 2023-03-21
                  • 2016-03-03
                  相关资源
                  最近更新 更多