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

che*_*hen 11 erlang functional-programming

作为函数式语言的新手(几周前我开始接触Erlang - 第一个可以让我开始使用的函数式语言).

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

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

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

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

Chr*_*ian 10

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

  1. 定义比效率更重要.也就是说,一个能够理解所有行为的函数的明显正确的实现比难以推理的复杂优化的函数更好.(并且应该尽可能地优先考虑;直到有证据证明必须打破这个不错的财产.)
  2. 数学函数没有副作用.有用的程序必须有副作用.函数式程序员知道副作用,这是一个非常危险和复杂的事情,并将程序设计为一组函数,从一个副作用中获取输出值并为下一个副作用创建输入值.

第一个与模糊相关:"优雅的代码".列表推导可以呈现非常简洁和数学方程式,如函数定义.只需看看LC实现的快速排序.这就是我如何定义优雅,简洁并使所有行为清晰.不是那种perl code-golf,你最常用的是简洁和神秘.

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

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

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


art*_*non 5

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

嗯,这不是关于"自上而下"或"自下而上"的设计.这是关注手头问题的"内容",而不是"如何".当我开始使用函数式编程时,我发现我一直在回想起forC中的嵌套循环等命令式构造.然后我很快发现尝试将我的命令式思维转换为函数式构造非常困难.我会试着给你一个更具体的例子.我将在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;
}
Run Code Online (Sandbox Code Playgroud)

跟踪我的思考过程:

  1. 思考一步.首先,接受用户的号码.让这个号码被调用inputNumber.scanf()写的.
  2. 基本算法:除非另有证明,否则数字为素数.primeFlag声明并设置等于1.
  3. 检查primeNumber2到2之间的每个数字primeNumber/2.for循环开始了.声明了i要检查的循环变量primeNumber.
  4. 为了反驳我们最初断言这个数字是素数,请检查primeNumber每个i.我们发现即使i是分裂的那一刻,也会primeNumber设置primeFlag0break.循环体写.
  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]
Run Code Online (Sandbox Code Playgroud)

跟踪我的思考过程:

  1. 如果一个数字没有除数而只有一个和它本身,那么它就是素数.所以,null divisors.
  2. 我们如何建立divisors?首先,让我们写下可能的候选人名单.写下德克萨斯范围从2到数/ 2.
  3. 现在,过滤列表,只挑选出真正除数的项目.写过滤器mod x y == 0

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

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

  • 我不明白 - 我的作品如何必要?我们解决问题的方法不同,但我的代码是纯粹的非monadic Haskell.没有踩踏,也没有副作用.您的方法首先生成素数列表,然后检查列表中是否找到给定元素.另一方面,我的功能方法类似于我的命令式方法 - 我这样做是为了比较和说明. (2认同)

bca*_*cat 4

\n

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

\n
\n\n

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

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

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

\n