代码之家  ›  专栏  ›  技术社区  ›  chen

学习“如何思考功能”的建议?

  •  11
  • chen  · 技术社区  · 17 年前

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

    我开始写一些小算法(比如 left_rotate_list , bubble_sort, merge_sort 等等)。我发现自己经常迷失在诸如“我应该使用辅助列表来存储中间结果吗?”和“我应该创建一个辅助函数来实现这一点吗?”之类的决策中

    过了一段时间,我发现函数式编程(如果我说的毫无意义,请耐心听我说)鼓励“自上而下”的设计:即,当我进行合并排序时,你首先写下所有合并排序步骤,并将其命名为单独的辅助函数;然后逐一实现这些辅助函数(如果需要进一步划分这些辅助函数,请使用相同的方法)。

    这似乎与面向对象设计有点矛盾,在面向对象设计中,你可以从底层开始构建基本的数据结构,然后将数据结构和算法组装成你想要的东西。

    谢谢你的评论。是的,我想得到关于如何“用函数式语言思考”的建议(就像“用Java思考”、“用C++思考”一样)。

    5 回复  |  直到 17 年前
        1
  •  10
  •   Christian    17 年前

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

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

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

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

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

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

        2
  •  5
  •   artagnon    17 年前

    for 然后我很快发现,试图将我的命令式思维转化为函数式构造是非常困难的。我会试着给你举一个更具体的例子。我将用C和Haskell实现一个等效的程序,并尝试在这两种情况下跟踪我的思维过程。请注意,为了解释的目的,我已经明确地说得很详细了。

    #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 .scan()已写入。
    2. primeFlag 1 .
    3. primeNumber 针对从2到3的每个数字 primeNumber/2 . 为了 循环开始了。声明了一个循环变量 i 检查 质数 反对。
    4. 要反驳我们最初的断言,即该数是素数,请检查 质数 反对每一个 我 .只要我们找到一个 我 分裂 质数 ,set primeFlag 到 0 和 break
    5. 在经过我们严格的检查流程后 为了 循环,检查以下值 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到数字/2。
    3. 现在,过滤列表,只挑选出真正是数字除数的项目。编写过滤器 mod x y == 0

    “用函数式语言思考”

        3
  •  4
  •   bcat    17 年前

    • 接下来,看看基数+1、基数+2的情况。最终,你应该看到一种模式(分解为子问题、解决子问题、组合子解决方案),它允许你编写一个通用的递归案例,而不是最终达到基本案例。
    • 分解成子问题很容易,但组合子解决方案有点难。您需要一种方法将两个排序数组合并为一个排序数组。
    • :)

    是 与面向对象编程不同,但在两者之间切换时,您不需要完全放弃现有的设计技术。

        4
  •  2
  •   ayrnieu    17 年前

    我发现自己经常迷失在诸如“我应该使用辅助列表来存储中间结果吗?”和“我应该创建一个辅助函数来实现这一点吗?”之类的决策中

    我对此的建议是:阅读 The Little Schemer 。您可以在Erlang中跟踪它。这是一本能让你深刻理解这一点的好书。

        5
  •  0
  •   ȷ̇c    17 年前

    它的重要性在于习惯于认为数据可以用作代码,以及 反之亦然 .

    过了一会儿,我发现它很实用 下”设计。

    我同意。