为什么在函数式编程中不鼓励使用赋值运算符或循环?


9

如果我的函数满足以下两个要求,我相信Sum 返回给定条件下列表项的总和的函数,其中项在给定条件下的评估结果为true,这称为纯函数,不是吗?

1)对于给定的一组i / p,无论何时调用函数,都将返回相同的o / p

2)它没有任何副作用

public int Sum(Func<int,bool> predicate, IEnumerable<int> numbers){
    int result = 0;
    foreach(var item in numbers)
        if(predicate(item)) result += item;
    return result;
}

范例: Sum(x=>x%2==0, new List<int> {1,2,3,4,5...100});

我之所以问这个问题,是因为我几乎每时每刻都在建议人们避免使用赋值运算符和循环,因为它是命令式编程风格。那么上面的示例在函数编程的上下文中使用循环和赋值运算符会出错吗?


1
它没有任何副作用 -当item变量在循环中发生突变时,它具有副作用。
法比奥

@Fabio好。但是您能否详细说明副作用的范围?
rahulaga_dev

Answers:


16

函数式编程有什么不同?

函数式编程原则上是声明性的。您说的结果而不是如何计算结果。

让我们看一下代码片段的实际功能实现。在Haskell中,它将是:

predsum pred numbers = sum (filter pred numbers)

是否清楚什么结果?相当是满足谓词的数字之和。如何计算?我不在乎,请问编译器。

您可能会说使用sumfilter是一个把戏,它并不重要。然后在没有这些帮助者的情况下实施它(尽管最好的方法是首先实施它们)。

sum递归不使用“功能编程101”解决方案:

sum pred list = 
    case list of
        [] -> 0
        h:t -> if pred h then h + sum pred t
                         else sum pred t

仍然很清楚单函数调用的结果是什么。是0recursive call + h or 0取决于pred h。即使最终结果不是立即显而易见的,它仍然很简单(尽管稍加练习,这实际上就像for循环一样)。

将其与您的版本进行比较:

public int Sum(Func<int,bool> predicate, IEnumerable<int> numbers){
    int result = 0;
    foreach(var item in numbers)
        if (predicate(item)) result += item;
    return result;
}

结果是什么?哦,我明白了:单一return声明,在这里没有惊喜return result

但是什么result呢?int result = 0?似乎不正确。您稍后再做些事情0。好的,您添加item。等等。

当然,对于大多数程序员来说,这很明显是在像这样的简单函数中发生的,但是要添加一些额外的return语句,这样一来,突然变得很难跟踪。所有的代码是关于如何什么留给读者找出- 这显然是一个非常迫切的风格

那么,变量和循环是否错误?

没有。

他们解释了很多事情,并且要求可变状态要快的许多算法。但是变量本质上是必须的,它解释了如何而不是什么,并且几乎没有预测它们的值,可能是在几行之后或在几次循环迭代之后。循环通常需要状态才有意义,因此它们本质上也是必不可少的。

变量和循环根本不是函数编程。

摘要

当代的泛函式编程比范式更具风格和思维方式。在这种心态中,强烈偏爱纯函数,但这实际上只是一小部分。

最广泛使用的语言允许您使用某些功能构造。例如,在Python中,您可以选择以下选项:

result = 0
for num in numbers:
    if pred(result):
        result += num
return result

要么

return sum(filter(pred, numbers))

要么

return sum(n for n in numbers if pred(n))

这些函数表达式非常适合此类问题,并且可以使代码更短(并且更短是好的)。您不应该无所顾忌地用它们替换命令性代码,但是当它们适合时,它们几乎总是一个更好的选择。


谢谢你的解释!!
rahulaga_dev

1
@RahulAgarwal您可能会发现此答案很有趣,它很好地抓住了建立真理与描述步骤的切线概念。我也喜欢短语“声明式语言包含副作用,而命令式语言则没有副作用”-通常,功能程序在处理外部世界(或执行某种优化算法)的有状态代码与纯功能代码之间具有清晰可见的分界线。
Frax

1
@Frax:谢谢!我会仔细看看的。最近还碰到了Rich Hickey关于价值观念的演讲,这确实很棒。我认为一条经验法则是“使用价值和表达,而不是使用拥有价值并可以改变的东西”
rahulaga_dev 18-10-26

1
@Frax:公平地说,FP是对命令式编程的抽象-因为最终必须有人指导机器“如何做”,对吗?如果是,那么与FP相比,命令式编程难道不具有更多的底层控制吗?
rahulaga_dev '18 -10-27

1
@Frax:我同意Rahul的观点,即命令更接近底层机器,它是较低级别的命令。如果硬件可以免费复制数据,那么我们就不需要进行破坏性更新来提高效率。从这个意义上说,命令式范式更接近于金属。
乔治

9

在函数编程中通常不建议使用可变状态。因此不建议使用循环,因为循环仅在与可变状态结合使用时才有用。

整个函数是纯净的,这很棒,但是函数式编程的范例不仅适用于整个函数的层次。您还希望在函数内部的局部级别上也避免可变状态。推理基本上是相同的:避免可变状态使代码更易于理解并防止某些错误。

就您而言,您可以编写numbers.Where(predicate).Sum()明显更简单的代码。更简单意味着更少的错误。


谢谢 !!我认为我缺少醒目的界限- 但函数编程的范例不仅适用于整个函数,而且现在我还想知道如何可视化此边界。从消费者的角度来看,基本上是纯函数,但是实际上编写此函数的开发人员没有遵循纯函数准则?困惑:(
rahulaga_dev

@RahulAgarwal:什么边界?
JacquesB '18 -10-21

从功能使用者的角度来看,如果编程范例有资格成为FP,我会感到困惑。Bcoz如果我查看Wherein的实现实现numbers.Where(predicate).Sum()-它利用了foreach循环。
rahulaga_dev 18-10-21

3
@RahulAgarwal:作为函数的使用者,只要函数或模块在外部是纯净的,您就不必在乎函数或模块在内部是否使用可变状态。
JacquesB '18 -10-21

7

从外部观察者的观点来看,您的Sum功能是纯净的,而内部实现显然不是纯净的-尽管您存储了状态,result并且您在其中反复进行了变异。避免发生可变状态的原因之一是因为它对程序员产生了更大的认知负担,进而导致更多错误[需要引用]

尽管在这样的简单示例中,可变状态的存储量可能足够小,不会引起任何严重的问题,但一般原理仍然适用。像一个玩具例子Sum可能不是说明了必要的函数式编程的优点的最好方式-尝试做一些有很多可变状态和优势可以更加清晰。

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.