在功能编程中“记住”值


20

我已经决定将学习函数式编程的工作推给自己。到目前为止,这是一个爆炸,我已经“看到了光”。不幸的是,我实际上不认识任何可以跳出问题的函数式程序员。介绍堆栈交换。

我正在上网络/软件开发课程,但是我的老师对函数式编程并不熟悉。他对我使用它很好,他只是请我帮助他了解它是如何工作的,以便他可以更好地阅读我的代码。

我认为最好的方法是通过说明一个简单的数学函数,例如将值提高到幂。从理论上讲,我可以使用预先构建的函数轻松地做到这一点,但这会违背示例的目的。

无论如何,我在弄清楚如何保持价值方面有些困难。由于这是函数式编程,因此我无法更改变量。如果我必须对它进行编码,它将看起来像这样:

(以下是所有伪代码)

f(x,y) {
  int z = x;
  for(int i = 0, i < y; i++){
    x = x * z;
  }
  return x;
}

在函数式编程中,我不确定。这是我想出的:

f(x,y,z){
  if z == 'null',
    f(x,y,x);
  else if y > 1,
    f(x*z,y-1,z);
  else
    return x;
}

这是正确的吗?z在这两种情况下,我都需要保留一个值,但是我不确定如何在函数编程中执行此操作。从理论上讲,我的工作方式有效,但是我不确定它是否“正确”。有更好的方法吗?


32
如果您想认真对待自己的例子,请它解决一个实际问题而不是数学问题。在开发人员中有些陈词滥调,“所有FP都擅长解决数学问题”,如果您的示例是“另一种数学功能”,则您只是在强化构造型,而不是使您所做的事情看起来有用。
梅森惠勒

12
考虑到现实世界的考虑,您的尝试实际上是相当不错的。您所有的递归调用都是尾调用,也就是说,该函数在调用它们之后不执行其他操作。这意味着支持它的编译器或解释器可以对其进行优化,以便您的递归函数使用固定数量的堆栈内存,而不是与比例的内存y
8bittree '16

1
非常感谢您的支持!我对此还很陌生,所以我的伪代码并不完美。@MasonWheeler我猜,在这种情况下,我的代码并不是真的应该被认真对待。我仍在学习,我之所以喜欢FP,是因为它是Math-y。我的示例的全部目的是向老师解释为什么我使用FP。他并不真正了解它的含义,因此这似乎是向他展示优势的好方法。
Ucenna

5
您打算用哪种语言编写代码?不要尝试使用不适合您所使用语言的样式。
Carsten S

Answers:


37

首先,祝贺“见光”。通过扩大视野,您已经使软件世界变得更加美好。

其次,老实说,除了“缩进看起来”这样的陈腐注释之外,不理解函数式编程的教授将无法对您的代码说出任何有用的东西。在Web开发课程中这并不奇怪,因为大多数Web开发都是使用HTML / CSS / JavaScript完成的。根据您实际上对学习Web开发的关心程度,您可能需要努力学习教授所教授的工具(尽管可能很痛苦-我从经验中知道)。

要解决上述问题:如果命令性代码使用循环,那么功能代码很可能是递归的。

(* raises x to the power of y *)
fun pow (x: real) (y: int) : real = 
    if y = 1 then x else x * (pow x (y-1))

注意,该算法实际上与命令性代码大致相同。实际上,可以将上面的循环视为迭代递归过程的语法糖。

附带说明,z实际上,在命令式或功能性代码中都不需要值。您应该这样编写命令式函数:

def pow(x, y):
    var ret = 1
    for (i = 0; i < y; i++)
         ret = ret * x
    return ret

而不是更改变量的含义x


您的递归pow不太正确。照原样,pow 3 3返回81,而不是27。它应该是else x * pow x (y-1).
8bittree '16

3
糟糕,编写正确的代码很难:)已修复,并且我还添加了类型注释。@Ucenna应该是SML,但是我已经有一段时间没有使用它了,所以我的语法可能有点错误。有太多令人难以置信的方法来声明一个函数,我永远不记得正确的关键字。除了语法更改外,代码在JavaScript中是相同的。
gardenhead

2
@jwg Javascript确实具有一些功能方面:函数可以定义嵌套函数,返回函数并接受函数作为参数;它支持带有词法作用域的闭包(尽管没有lisp动态作用域)。避免更改状态和更改数据取决于程序员的纪律。
卡斯珀范登伯格

1
@jwg没有“功能”语言(“命令式”,“面向对象”或“声明式”)的一致定义。我尽量避免使用这些术语。阳光下有太多的语言无法归类为四类。
gardenhead

1
流行度是一个可怕的指标,这就是为什么每当有人提到语言或工具X必须更好时,因为它被广泛使用的原因,我知道继续争论是没有意义的。我比Haskell更熟悉ML语言系列。但是我也不确定这是真的。我的猜测是绝大多数开发人员都没有尝试过 Haskell。
gardenhead

33

这实际上只是Gardenhead答案的附录,但我想指出的是,您看到的图案有一个名称:可折叠。

在函数式编程中,折叠是一种组合一系列值的方法,这些值“记住”每个操作之间的值。考虑强制添加数字列表:

def sum_all(xs):
  total = 0
  for x in xs:
    total = total + x
  return total

我们采取的值的列表xs初始状态0(由下式表示total在这种情况下)。然后,对于xin 中的每个xs值,我们根据一些合并操作(在本例中为加法)将该值与当前状态合并,并将结果用作状态。从本质上讲,sum_all([1, 2, 3])等效于(3 + (2 + (1 + 0)))。可以将这种模式提取到一个高阶函数中,该函数接受函数作为参数:

def fold(items, initial_state, combiner_func):
  state = initial_state
  for item in items:
    state = combiner_func(item, state)
  return state

def sum_all(xs):
  return fold(xs, 0, lambda x y: x + y)

的实现fold仍然势在必行,但是也可以递归地完成:

def fold_recursive(items, initial_state, combiner_func):
  if not is_empty(items):
    state = combiner_func(initial_state, first_item(items))
    return fold_recursive(rest_items(items), state, combiner_func)
  else:
    return initial_state

用折叠表示,您的功能很简单:

def exponent(base, power):
  return fold(repeat(base, power), 1, lambda x y: x * y))

...其中repeat(x, n)返回的n副本列表x

许多语言,特别是那些针对函数式编程的语言,都在其标准库中提供折叠功能。甚至Javascript也以名称提供它reduce。通常,如果发现自己使用递归在某种循环中“记住”某个值,则可能需要折叠。


8
一定要学会发现可以通过折叠或地图解决问题的时间。在FP中,几乎所有循环都可以表示为fold或map;因此,通常无需显式递归。
Carcigenicate's

1
在某些语言中,您可以编写fold(repeat(base, power), 1, *)
user253751 '16

4
Rico Kahler:scan本质上是fold在这里,不仅仅是将值列表组合为一个值,而是将其组合在一起,并沿途吐出每个中间值,生成所有中间状态列表,该中间状态是折叠创建的,而不仅仅是生成最终状态。就fold每个循环操作而言,它都是可以实现的。
杰克

4
@RicoKahler而且,据我所知,折减是一回事。Haskell使用“折叠”一词,而Clojure则使用“减少”一词。在我看来,他们的行为是一样的。
Carcigenicate's

2
@Ucenna:它是变量和函数。在函数式编程中,函数就像数字和字符串一样是值-您可以将它们存储在变量中,将它们作为参数传递给其他函数,从函数中返回它们,并且通常将它们像其他值一样对待。combiner_func一个参数sum_all也是如此,它传递了一个匿名函数lambda即位-它创建函数值而不命名它),该函数定义了如何将两个项目组合在一起。
杰克

8

这是一个补充性答案,有助于解释地图和折痕。对于下面的示例,我将使用此列表。请记住,此列表是不可变的,因此它将永远不会改变:

var numbers = [1, 2, 3, 4, 5]

我将在示例中使用数字,因为它们会导致易于阅读的代码。但是请记住,折叠可用于传统命令式循环可用于的任何事情。

一个地图需要的东西的清单,和一个函数,并返回使用该功能修改的列表。每一项都传递给函数,并成为函数返回的值。

最简单的例子就是在列表中的每个数字上添加一个数字。我将使用伪代码使其与语言无关:

function add-two(n):
    return n + 2

var numbers2 =
    map(add-two, numbers) 

如果打印了numbers2,您将看到[3, 4, 5, 6, 7]哪个是第一个列表,每个列表中添加了2个元素。注意,该功能add-two已被允许map使用。

折叠与相似,只是您需要赋予它们的函数必须带有2个参数。第一个参数通常是累加器(最常见的是左折)。累加器是循环时传递的数据。第二个参数是列表的当前项目。就像上面的map功能一样。

function add-together(n1, n2):
    return n1 + n2

var sum =
    fold(add-together, 0, numbers)

如果打印,sum您将看到数字列表的总和:15。

这是要fold执行的参数:

  1. 这就是我们提供的功能。折叠将把当前累加器和列表的当前项目传递给函数。函数返回的结果将成为新的累加器,下一次将传递给该函数。这是在循环FP样式时“记住”值的方式。我给了它一个接受2个数字并将其相加的函数。

  2. 这是初始累加器;在处理列表中的任何项目之前,累加器的启动方式。在对数字求和时,将数字相加之前的总和是多少?0,我将其作为第二个参数传递。

  3. 最后,与地图一样,我们也将数字列表传递给地图进行处理。

如果仍然没有折痕,请考虑这一点。当你写:

# Notice I passed the plus operator directly this time, 
#  instead of wrapping it in another function. 
fold(+, 0, numbers)

您基本上是将传递的函数放在列表中的每个项目之间,并将初始累加器添加到左侧或右侧(取决于它是左折还是右折),因此:

[1, 2, 3, 4, 5]

成为:

0 + 1 + 2 + 3 + 4 + 5
^ Note the initial accumulator being added onto the left (for a left fold).

等于15

map当您想将一个列表变成相同长度的另一个列表时,请使用a 。

fold当您要将列表变成单个值时,请使用a ,例如将数字列表求和。

正如@Jorg在评论中指出的那样,“单个值”不必像数字一样简单。它可以是任何单个对象,包括列表或元组!我其实是有褶皱点击我的方法是定义地图来讲折叠。注意累加器是如何列出的:

function map(f, list):
    fold(
        function(xs, x): # xs is the list that has been processed so far
            xs.add( f(x) ) # Add returns the list instead of mutating it
        , [] # Before any of the list has been processed, we have an empty list
        , list) 

老实说,一旦您理解了每种循环,您就会意识到几乎所有循环都可以用折叠或地图代替。


1
@Ucenna @Ucenna您的代码有一些缺陷(例如i从未定义过),但是我认为您有正确的主意。您的示例的一个问题是:函数(x)一次仅传递列表的一个元素,而不传递整个列表。第一次x调用时,它已通过初始累加器(y)作为第一个参数,而第一个元素则作为第二个参数。下次运行时,x将在左侧传递新的累加器(无论x第一次返回的是什么),并将列表的第二个元素作为第二个参数传递。
Carcigenicate's

1
@Ucenna现在您已经有了基本概念,请再次查看Jack的实现。
Carcigenicate's

1
@Ucenna:不幸的是,不同的lang对于赋予fold的函数将累加器作为第一个还是第二个参数有不同的偏好。使用加法之类的可交换操作教折的好原因之一。
2016年

3
fold当您要将列表变成单个值时(例如,将数字列表求和),请使用a。” –我只想指出,此“单个值”可以任意复杂,包括列表!实际上,这fold是一种通用的迭代方法,它可以完成迭代可以做的所有事情。例如,map可以简单地表示为func map(f, l) = fold((xs, x) => append(xs, f(x)), [], l)此处,由计算的“单个值” fold实际上是一个列表。
约尔格W¯¯米塔格

2
…可能想做一个清单,可以做fold。它不必是列表,每个可以表示为空/不为空的集合都可以。这基本上意味着任何迭代器都可以。(我猜会有抛单词“catamorphism”太多对于一个初学者的介绍,虽然:-D)
约尔格W¯¯米塔格

1

很难找到内置功能无法解决的好问题。并且如果它是内置的,那么它应该被用作x语言中良好风格的示例。

例如,在haskell中,您已经具有(^)Prelude中的功能。

或者,如果您想以更多编程方式进行操作 product (replicate y x)

我的意思是,如果不使用样式/语言提供的功能,则很难显示其优势。但是,这可能是迈向显示幕后工作方式的一个好步骤,但是我认为您应该以最佳语言编写所使用的任何语言,然后帮助那里的人了解所发生的事情。


1
为了在逻辑上将此答案与其他答案相关联,应注意,这product只是将fold乘法作为函数且将1作为其初始参数的快捷函数,并且该replicate函数会产生迭代器(或列表;如我所指出的那样)两者之间在haskell中基本上是无法区分的),从而给出了给定数量的相同输出。现在,应该很容易理解该实现如何与上述@Jack的答案做同样的事情,只是使用相同功能的预定义特殊情况版本以使其更加简洁。
佩里亚塔·布雷塔
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.