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

时间:2009-10-10 20:39:08

标签: functional-programming erlang

作为函数式语言的新手(几周前我开始接触Erlang - 第一种可以让我动手的功能语言)。

我开始编写一些小算法(例如left_rotate_listbubble_sort, merge_sort等。我发现自己常常迷失在诸如“我应该使用帮助列表进行中间结果存储吗?”等决策中。并且“我应该创建一个辅助函数来执行此操作吗?”

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

这似乎与OO设计略有矛盾,您可以从底部开始构建基本数据结构,然后将数据结构和算法组合成您想要的。

感谢您的评论。是的,我希望得到关于如何“用函数式语言思考”的建议(就像“用Java思考”,“用C ++思考”)。

5 个答案:

答案 0 :(得分:10)

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

  1. 定义比效率更重要。也就是说,一个能够理解所有行为的函数的明显正确的实现比难以推理的复杂优化的函数更好。 (并且应该尽可能地优先考虑;直到有证据表明必须打破这个不错的财产。)
  2. 数学函数没有副作用。有用的程序必须有副作用。函数式程序员知道副作用,这是一个非常危险和复杂的事情,并将程序设计为一组函数,从一个副作用中获取输出值并为下一个副作用创建输入值。
  3. 第一名与模糊相关:“优雅代码”。列表推导可以呈现非常简洁和数学方程式,如函数定义。只需看看LC实现的快速排序。这就是我如何定义优雅,简洁并使所有行为清晰。不是perl code-golf,你最常用的是简洁和神秘。

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

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

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

答案 1 :(得分:5)

  

过了一会儿,我发现函数式编程鼓励了“自上而下”的设计。

嗯,这不是关于“自上而下”或“自下而上”的设计。这是关注手头问题的“内容”,而不是“如何”。当我开始使用函数式编程时,我发现我一直在回想起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. 针对从2到primeNumber的每个号码检查primeNumber/2for循环开始了。声明了一个循环变量i来检查primeNumber
  4. 要反驳我们关于该数字为素数的初始断言,请针对每个primeNumber检查i。我们找到一个i除以primeNumber的时刻,将primeFlag设置为0break。循环体写。
  5. for循环中完成严格的检查过程后,请检查primeFlag的值并将其报告给用户。 printf()写的。
  6. 在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
    4.   

      我想得到关于如何做的建议   “用功能语言思考”

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

答案 2 :(得分:4)

  

过了一会儿,我发现函数式编程鼓励了“自上而下”的设计。

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

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

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

答案 3 :(得分:2)

  

我发现自己常常迷失在诸如“我应该使用帮助列表进行中间结果存储吗?”等决策中。并且“我应该创建一个辅助函数来执行此操作吗?”

我对此的建议:阅读The Little Schemer。你可以在Erlang中关注它。这是一本很好的书,可以将这种感觉融入到您的手中。

答案 4 :(得分:0)

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

通常你使用几个原始操作(折叠,嵌套,线程,分发......,以及一些是通用内部产品,外部产品等)构建一个程序(数据),并使用这个程序(数据)来操纵其他数据。

  

过了一会儿,我发现功能正常   编程[...]鼓励“顶级   “设计。

我同意。