【问题标题】:Advice on Learning "How to Think Functional"?关于学习“如何思考功能”的建议?
【发布时间】:2023-03-11 10:00:02
【问题描述】:

作为函数式语言的新手(几周前我开始接触 Erlang —— 这是我能接触到的第一个函数式语言)。

我开始写一些小算法(如left_rotate_listbubble_sort,merge_sort等)。我发现自己经常迷失在诸如“我应该使用辅助列表来存储中间结果吗?”之类的决定中。和“我应该创建一个辅助函数来执行此操作吗?”

过了一段时间,我发现函数式编程(如果我说的根本没有意义,请耐心等待)鼓励“自上而下”的设计:即,当我执行 merge_sort 时,您首先写下所有合并对步骤进行排序,并将它们命名为单独的辅助函数;然后你一个一个地实现这些辅助函数(如果你需要进一步划分这些辅助函数,用同样的方法来做)。

这似乎和OO设计有点矛盾,你可以从底层开始构建基本的数据结构,然后将数据结构和算法组装成你想要的。

感谢 cmets。是的,我想获得有关如何“用函数式语言思考”的建议(就像“用 Java 思考”、“用 C++ 思考”)。

【问题讨论】:

  • 您想要“面向 OOP 程序员的函数式编程指南”的建议吗?
  • Mahin,更新问题。感谢您的回复。

标签: functional-programming erlang


【解决方案1】:

答案是函数式编程是使用函数进行编程,因为它们是在数学中定义的(简而言之,将值从域映射到共域的无副作用的东西)。将其实际转化为“如何思考”是挥手的部分,很难详尽无遗,但我会总结一下我的一些想法:

  1. 定义比效率更重要。也就是说,一个可以理解所有行为的函数的明显正确的实现比一个难以推理的复杂优化的更好。 (并且应该尽可能长时间地被首选;直到有证据表明必须破坏这个好属性。)
  2. 数学函数没有副作用。一个有用的程序必须有副作用。函数式程序员意识到副作用是一件非常危险和复杂的事情,并将程序设计为一堆函数,这些函数从一个副作用获取输出值并为下一个副作用创建输入值。

第一与模糊相关:“优雅的代码”。列表推导可以呈现非常简洁和数学方程,如函数定义。看看用 LC 实现的快速排序。这就是我如何定义优雅、简洁并使所有行为清晰。不是你最经常简洁和神秘的 perl 代码高尔夫。

第二个是我在所有编程中日常使用的东西。将代码划分为当前状态的函数(方法、例程等),这些函数是无副作用的计算,为下一个要采取的行动(即使是下一个要采取的行动)提供输入。返回值后,将其交给执行所描述操作的例程,然后重新开始。

在我的脑海中,我将一个 Erlang 进程描绘为一个状态机图,其中每个顶点都是一个副作用和一个函数,其输出是从顶点中选择哪条边。对副作用的高度重视是函数式编程范式教会我的。特别是在 Erlang 中,因为副作用在并发中确实很重要,而 Erlang 使并发非常可用。

就像一些孤立的部落只有一个词来表示大于 3 的数字,或者没有词来表示“我的”/“你的”。感觉就像流行语言没有“这会导致副作用”的词,但函数式编程有。它迫使你时刻意识到这一点,这是一件好事。

【讨论】:

    【解决方案2】:

    一段时间后,我发现函数式编程 [...] 鼓励“自上而下”的设计。

    嗯,这实际上与“自上而下”或“自下而上”的设计无关。这是关于关注手头问题的“什么”,而不是“如何”。当我开始使用函数式编程时,我发现我一直在回忆诸如 C 中嵌套的 for 循环之类的命令式构造。然后我很快发现尝试将我的命令式思维转换为函数式构造非常困难。我会试着给你一个更具体的例子。我将在 C 和 Haskell 中实现一个等效的程序,并尝试在这两种情况下跟踪我的思维过程。请注意,出于解释的目的,我已经明确详细说明了。

    在 C 中:

    #include <stdio.h>
    
    int main(void)
    {
      int i, inputNumber, primeFlag = 1;
      scanf("%d", &inputNumber);
      for(i = 2; i <= inputNumber / 2; i ++)
        {
          if (inputNumber % i == 0)
            {
              primeFlag = 0;
              break;
            }
        }
      if (primeFlag == 0) printf("False\n");
      else printf ("True\n");
      return 0;
    }
    

    我的思考过程的痕迹:

    1. 分步思考。首先,接受用户的号码。将此号码称为inputNumber。 scanf() 写的。
    2. 基本算法:除非另有证明,否则数字是素数。 primeFlag 声明并设置为等于1
    3. 检查 primeNumber 与从 2 到 primeNumber/2 的每个数字。 for 循环开始。声明了一个循环变量 i 来检查 primeNumber 是否。
    4. 要反驳我们最初关于该数是素数的断言,请检查primeNumber 和每个i。当我们找到一个可以分割primeNumberi 时,将primeFlag 设置为0break。循环体写入。
    5. for循环中经过我们严格的检查过程后,检查primeFlag的值并报告给用户。 printf() 写的。

    在 Haskell 中:

    assertPrime :: (Integral a) => a -> Bool
    assertPrime x = null divisors
        where divisors = takeWhile (<= div x 2) [y | y <- [2..], mod x y == 0]
    

    我的思考过程的痕迹:

    1. 一个数是素数,如果它没有除数,只有一个和它自己。所以,null divisors
    2. 我们如何构建divisors?首先,让我们写下可能的候选人列表。写下德州范围从 2 到 number/2。
    3. 现在,过滤列表,只挑选出真正是数字除数的项目。写了过滤器mod x y == 0

    我想就如何获得建议 “用函数式语言思考”

    好的,首先,考虑“什么”,而不是“如何”。这可能需要大量练习才能习惯。另外,如果您以前是像我这样的 C/C++ 程序员,请不要担心内存!现代语言有一个垃圾收集器,它是为你编写的——所以甚至不要试图修改变量。对我个人有帮助的另一件事是:在程序中写下类似英语的定义,以抽象出执行繁重工作的函数。

    【讨论】:

    • 你的函数式示例难道不是伪装成函数式的命令式代码吗?这样的事情不是更能描述你的意图吗? assertPrime x = x elem primes xprimes x = sieve [1..x],其中 sieve 是朴素素数生成器?
    • 我不明白——我的作品有什么必要性?我们解决问题的方法不同,但我的代码是纯非单子 Haskell。没有踩踏,也没有副作用。您的方法首先生成一个素数列表,然后检查是否在列表中找到给定元素。另一方面,我的函数式方法类似于我的命令式方法——我这样做是为了比较和说明。
    【解决方案3】:

    一段时间后,我发现函数式编程 […] 鼓励“自上而下”的设计。

    我不确定这是一个准确的说法。我最近一直在尝试自学函数式编程,我发现一种“自下而上”的编程风格确实对我有帮助。要使用您的合并排序示例:

    • 首先查看基本情况。如何对 0/1 元素的数组进行排序?
    • 接下来,查看基数 + 1、基数 + 2、……案例。最终,您应该会看到一种模式(拆分为子问题、解决子问题、组合子解决方案),该模式允许您编写一般递归案例,而不是最终达到基本案例。
    • 拆分成子问题很容易,但合并子解决方案有点困难。您需要一种将两个排序数组合并为一个排序数组的方法。
    • 现在把所有东西放在一起。恭喜,您刚刚编写了归并排序。 :)

    我可能误用了这个词,但这对我来说就像是自下而上的设计。函数式编程不同于面向对象的编程,但在两者之间切换时,您不需要完全放弃现有的设计技术。

    【讨论】:

      【解决方案4】:

      我发现自己经常迷失在诸如“我应该使用辅助列表来存储中间结果吗?”之类的决定中。和“我应该创建一个辅助函数来执行此操作吗?”

      对此我的建议:阅读The Little Schemer。你可以在 Erlang 中关注它。这是一本让你深入了解这一点的好书。

      【讨论】:

        【解决方案5】:

        习惯于认为数据可以用作代码是很重要的,反之亦然

        通常你使用几个原始操作(折叠,嵌套,线程,分布,......,有些是广义的内积,外积等)来构造一个程序(数据),并使用这个程序(数据)来操纵其他数据。

        一段时间后,我发现功能 编程 […] 鼓励“顶级 向下”的设计。

        我同意。

        【讨论】:

          猜你喜欢
          • 2020-07-29
          • 2011-08-17
          • 1970-01-01
          • 2011-05-23
          • 1970-01-01
          • 2014-09-21
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多