没有可变状态怎么办?


265

最近,我已经阅读了很多有关函数式编程的文章,并且我能理解其中的大部分内容,但是我无能为力的一件事就是无状态编码。在我看来,通过删除可变状态来简化编程就像通过删除仪表板来“简化”汽车:最终产品可能更简单,但好运使其与最终用户互动。

我能想到的几乎每个用户应用程序都将状态作为核心概念。如果您写文档(或SO帖子),则状态随着每次新输入而改变。或者,如果您玩电子游戏,则有很多状态变量,从所有角色的位置开始,这些角色往往会不断移动。在不跟踪值更改的情况下,您怎么可能做些有用的事情?

每次我找到讨论此问题的内容时,都是用真正的技术功能性的语言编写的,这些功能假定我没有丰富的FP背景。有谁知道一种方法来向对命令性编码有很好而扎实的理解的人解释这一点,但是在功能方面谁是完整的n00b?

编辑:到目前为止,很多答复似乎都在试图说服我不变价值的优势。我明白了。这是很合理的。我不明白的是,如何在没有可变变量的情况下跟踪必须更改的值并不断更改。



1
我个人的拙见是,这就像力量和金钱。收益递减法则适用。如果你很坚强,可能会没有动力去稍微变得强壮,但是努力工作并没有什么坏处(有些人会充满激情)。全局可变状态也是如此。我个人倾向于接受随着我的编码技能的进步,最好限制代码中全局可变状态的数量。它可能永远都不是完美的,但是努力使全球可变状态最小化是一件好事。
AturSams

就像使用金钱一样,将更多的时间投入到金钱上将达到一个目的,它不再是非常有用的,其他优先事项将升至最高。例如,如果您达到最大的力量(按照我的隐喻),它可能没有任何用处,甚至可能成为负担。但是,努力实现这一可能无法实现的目标并向其中投入适度的资源仍然是一件好事。
AturSams

7
简而言之,在FP中,函数从不修改状态。最终,他们将返回替换当前状态的内容。但是状态永远不会就地修改(变异)。
jinglesthula

有很多方法可以使状态保持不变(使用我所了解的堆栈),但是从某种意义上讲,这个问题是有意义的(即使这是一个很好的问题)。很难简单地谈论,但是这是一篇帖子,希望可以回答您的问题medium.com/@jbmilgrom/…。TLDR的含义是,即使一个有状态的功能程序的语义也是不可变的,但是程序功能的通信黑白运行得以处理。
jbmilgrom

Answers:


166

或者,如果您玩电子游戏,则有很多状态变量,从所有角色的位置开始,这些角色往往会不断移动。在不跟踪值更改的情况下,您怎么可能做些有用的事情?

如果您有兴趣,这里有一系列描述Erlang游戏编程的文章。

您可能不喜欢这个答案,但是直到使用它,您都不会得到功能程序。我可以张贴代码示例并说“在这里,您看不到 ”-但是,如果您不了解语法和基本原理,那么您的眼睛就会蒙上眼睛。从您的角度来看,似乎我在做与命令式语言相同的事情,但是只是设置各种边界以有目的地使编程变得更加困难。我的观点是,您只是在经历Blub悖论

一开始我很怀疑,但是几年前我跳入了函数式编程的火车,并爱上了它。函数式编程的诀窍是能够识别模式,特定的变量分配,并将命令状态移至堆栈。例如,for循环变为递归:

// Imperative
let printTo x =
    for a in 1 .. x do
        printfn "%i" a

// Recursive
let printTo x =
    let rec loop a = if a <= x then printfn "%i" a; loop (a + 1)
    loop 1

它不是很漂亮,但是我们得到了相同的效果而没有突变。当然,只要有可能,我们都希望避免完全循环,而只需将其抽象掉即可:

// Preferred
let printTo x = seq { 1 .. x } |> Seq.iter (fun a -> printfn "%i" a)

Seq.iter方法将枚举整个集合,并为每个项目调用匿名函数。非常便利 :)

我知道,打印数字并不十分令人印象深刻。但是,我们可以对游戏使用相同的方法:将所有状态保留在堆栈中,并使用递归调用中的更改创建一个新对象。通过这种方式,每个帧都是游戏的无状态快照,其中,每个帧仅创建一个全新的对象,并对任何需要更新的无状态对象进行所需的更改。伪代码可能是:

// imperative version
pacman = new pacman(0, 0)
while true
    if key = UP then pacman.y++
    elif key = DOWN then pacman.y--
    elif key = LEFT then pacman.x--
    elif key = UP then pacman.x++
    render(pacman)

// functional version
let rec loop pacman =
    render(pacman)
    let x, y = switch(key)
        case LEFT: pacman.x - 1, pacman.y
        case RIGHT: pacman.x + 1, pacman.y
        case UP: pacman.x, pacman.y - 1
        case DOWN: pacman.x, pacman.y + 1
    loop(new pacman(x, y))

命令版本和功能版本相同,但是功能版本显然不使用任何可变状态。功能代码将所有状态保持在堆栈上-这种方法的好处是,如果出现问题,调试很容易,您只需要堆栈跟踪即可。

这可以扩展到游戏中任意数量的对象,因为所有对象(或相关对象的集合)都可以在其自己的线程中呈现。

我能想到的几乎每个用户应用程序都将状态作为核心概念。

在功能语言中,我们无需更改对象的状态,而只是返回具有所需更改的新对象。它比听起来更有效。例如,数据结构非常容易表示为不变的数据结构。例如,众所周知,堆栈很容易实现:

using System;

namespace ConsoleApplication1
{
    static class Stack
    {
        public static Stack<T> Cons<T>(T hd, Stack<T> tl) { return new Stack<T>(hd, tl); }
        public static Stack<T> Append<T>(Stack<T> x, Stack<T> y)
        {
            return x == null ? y : Cons(x.Head, Append(x.Tail, y));
        }
        public static void Iter<T>(Stack<T> x, Action<T> f) { if (x != null) { f(x.Head); Iter(x.Tail, f); } }
    }

    class Stack<T>
    {
        public readonly T Head;
        public readonly Stack<T> Tail;
        public Stack(T hd, Stack<T> tl)
        {
            this.Head = hd;
            this.Tail = tl;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Stack<int> x = Stack.Cons(1, Stack.Cons(2, Stack.Cons(3, Stack.Cons(4, null))));
            Stack<int> y = Stack.Cons(5, Stack.Cons(6, Stack.Cons(7, Stack.Cons(8, null))));
            Stack<int> z = Stack.Append(x, y);
            Stack.Iter(z, a => Console.WriteLine(a));
            Console.ReadKey(true);
        }
    }
}

上面的代码构造了两个不可变列表,将它们附加在一起以创建一个新列表,然后附加结果。在应用程序中的任何地方都不会使用可变状态。它看起来有点笨重,但这仅仅是因为C#是一种冗长的语言。这是F#中的等效程序:

type 'a stack =
    | Cons of 'a * 'a stack
    | Nil

let rec append x y =
    match x with
    | Cons(hd, tl) -> Cons(hd, append tl y)
    | Nil -> y

let rec iter f = function
    | Cons(hd, tl) -> f(hd); iter f tl
    | Nil -> ()

let x = Cons(1, Cons(2, Cons(3, Cons(4, Nil))))
let y = Cons(5, Cons(6, Cons(7, Cons(8, Nil))))
let z = append x y
iter (fun a -> printfn "%i" a) z

创建和操作列表没有可变的必要。几乎所有数据结构都可以轻松转换为它们的功能等效项。我在这里写了一个页面该页面提供了堆栈,队列,左派堆,红黑树,惰性列表的不可变实现。没有一个代码片段包含任何可变状态。要“变异”一棵树,我要创建一个带有所需新节点的全新树-这非常有效,因为我不需要复制树中的每个节点,因此可以在新节点中重用旧节点树。

使用一个更重要的例子,我还编写了这个SQL解析器,它完全是无状态的(或者至少我的代码是无状态的,我不知道底层的词法库是否是无状态的)。

无状态编程与有状态编程一样具有表现力和功能,只需要进行一些练习就可以训练自己开始无状态思考。当然,“尽可能进行无状态编程,必要时进行有状态编程”似乎是大多数不纯函数语言的座右铭。当功能性方法不是那么干净或高效时,回落到可变对象上没有任何危害。


7
我喜欢吃豆子的例子。但这只能解决一个问题,而又引发另一个问题:如果其他问题引用了现有的Pacman对象,该怎么办?这样就不会再进行垃圾回收和替换了。相反,您最终得到该对象的两个副本,其中一个无效。您如何处理这个问题?
梅森惠勒2009年

9
显然,您需要使用新的Pacman对象创建一个新的“其他”对象;)当然,如果我们走的太远,最终每次发生变化时,我们都会为整个世界重新创建对象图。此处介绍了一种更好的方法(prog21.dadgum.com/26.html):与其让对象更新自身及其所有依赖关系,不如让对象将有关其状态的消息传递给处理所有事件的事件循环要容易得多。更新。这使得决定图中哪些对象需要更新,哪些对象不需要更新更加容易。
朱丽叶2009年

6
@Juliet,我有一个疑问-在我完全命令式的心态中,递归必须在某个时候结束,否则最终会产生堆栈溢出。在递归的pacman示例中,如何使堆栈保持空白-在函数的开头隐式弹出对象?
BlueStrat

9
@BlueStrat-好问题...如果是“尾调用” ...即递归调用是函数中的最后一件事...则系统不需要生成新的堆栈框架...可以只需重用上一个。这是对函数式编程语言的常见优化。en.wikipedia.org/wiki/Tail_call
reteptilian,2015年

4
@MichaelOsofsky,当与数据库和API进行交互时,总是有一个“外部世界”与之通信。在这种情况下,您将无法100%正常运行。重要的是,必须将这种“无法正常运行的”代码隔离开并抽象化,以使外界只有一个入口和一个出口。这样,您就可以保持其余代码的功能。
Chielt'4

76

简短答案:您不能。

那么,关于不变性的大惊小怪呢?

如果您精通命令式语言,那么您就会知道“全局变量是不好的”。为什么?因为它们会在代码中引入(或可能引入)一些非常难以解决的依赖关系。依赖性不好;您希望代码是模块化的。程序的某些部分不会影响其他部分。和FP为您带来了模块化的圣杯:无副作用,在所有。您只有f(x)= y。放入x,取出y。没有对x或其他任何更改。FP使您停止思考状态,而开始思考价值。您所有的函数都只是接收值并产生新值。

这具有几个优点。

首先,没有副作用意味着程序更简单,推理也更容易。不用担心引入程序的新部分会干扰和崩溃现有的工作部分。

其次,这使得程序几乎可以并行化(高效并行化是另一回事)。

第三,有一些可能的性能优势。说你有一个功能:

double x = 2 * x

现在,您输入了3英寸的值,而您得到了6英寸的值。每次。但是您也可以一定要这样做,对吗?是的 但是问题在于,当务之急是您可以做更多的事情。我可以:

int y = 2;
int double(x){ return x * y; }

但我也可以

int y = 2;
int double(x){ return x * (y++); }

命令式编译器不知道我是否会产生副作用,这使优化变得更加困难(例如,双2不必每次都为4)。功能上的人知道我不会-因此,每次看到“ double 2”,它都可以进行优化。

现在,尽管就计算机内存而言,每次创建新值对于复杂类型的值而言似乎都是非常浪费的,但不必如此。因为,如果您有f(x)= y,并且x和y的值“几乎相同”(例如,只有几片叶子不同的树),则x和y可以共享部分内存-因为它们都不会发生突变。

因此,如果这个不可改变的事情是如此出色,那么我为什么回答说没有可变状态就无法做任何有用的事情。好吧,没有可变性,您的整个程序将是一个巨大的f(x)= y函数。程序的所有部分也是如此:只是函数,还有所谓的“纯”函数。如我所说,这意味着每次 f(x)= y 。因此,例如readFile(“ myFile.txt”)每次都需要返回相同的字符串值。不太有用。

因此,每个FP提供一些改变状态的方法。“纯”功能语言(例如Haskell)使用一些令人恐惧的概念(例如monad)来完成此操作,而“不纯”功能语言(例如ML)直接允许这样做。

当然,功能语言还带有许多其他优点,这些优点使编程更加高效,例如一流的函数等。


2
<< readFile(“ myFile.txt”)每次都需要返回相同的字符串值。不太有用。>>我想只要隐藏全局文件系统,它就很有用。如果您将其视为第二个参数,并且让其他进程在每次使用filesystem2 = write(filesystem1,fd,pos,“ string”)对其进行修改时都返回对文件系统的新引用,并让所有进程交换对文件系统的引用,我们可以更清晰地了解操作系统。
eel ghEEz

@eelghEEz,这与Datomic对数据库采取的方法相同。
杰森

1
+1为范式之间的简洁明了的比较。一个建议是int double(x){ return x * (++y); }因为当前一个静止将4中,尽管仍具有未公开的副作用,而++y将返回6.
BrainFRZ

@eelghEEz我不确定是否有其他选择,真的,还有其他人吗?要将信息引入(纯)FP上下文,请“进行测量”,例如“在时间戳X处,温度为Y”。如果有人要求温度,他们可能隐含的意思是X = now,但是他们可能不可能要求温度是时间的通用函数,对吧?FP处理不可变状态,您必须从内部外部资源中通过可变状态创建一个不可变状态。索引,时间戳等很有用,但与可变性正交-就像VCS本身就是版本控制一样。
约翰·P

29

注意,说函数式编程没有“状态”是有点误导,可能是造成混乱的原因。它绝对没有“可变状态”,但是它仍然可以具有可操纵的值。它们只是不能就地更改(例如,您必须从旧值创建新值)。

这是一个过分的简化,但是假设您有一种OO语言,其中类的所有属性仅在构造函数中设置一次,所有方法都是静态函数。您仍然可以通过使方法采用包含其计算所需的所有值的对象,然后返回带有结果的新对象(甚至可能是同一对象的新实例)来执行几乎任何计算。

将现有的代码转换为这种范例可能很“困难”,但这是因为它确实需要一种完全不同的代码思考方式。作为副作用,尽管在大多数情况下,您可以免费获得大量并行处理机会。

附录:( 关于如何跟踪需要更改的值的编辑)
它们将被存储在一个不变的数据结构中……

这不是建议的“解决方案”,但最简单的方法是,可以将这些不可变的值存储到像“结构”这样的映射(字典/哈希表)中,并以“变量名”作为关键字。

显然,在实际的解决方案中,您将使用一种更为理智的方法,但这确实表明,在最坏的情况下,如果没有其他工作,您可以使用在调用树中随身携带的映射来“模拟”可变状态。


2
好的,我更改了标题。但是,您的答案似乎会导致更严重的问题。如果每次状态更改时都必须重新创建每个对象,那么我将花费所有CPU时间,除了构造对象之外,什么也不做。我在这里考虑的是游戏编程,在这里,您需要同时在屏幕(和屏幕外)上移动很多东西,这些东西需要能够彼此交互。整个引擎具有固定的帧速率:您要做的所有事情,都必须在X毫秒内完成。当然有比不断回收整个对象更好的方法吗?
梅森惠勒

4
它的优点在于不可改变性取决于语言,而不取决于实现。通过一些技巧,您可以在语言中拥有不可更改的状态,而实现实际上是在改变状态。例如,参见Haskell的ST单子。
CesarB

4
@Mason:关键是,编译器比您可以更好地决定就地更改状态在(线程)安全的位置。
jerryjvl

我认为,对于游戏,您应该避免速度无关紧要的任何部分都保持不变。虽然不变的语言可能会为您优化,但没有什么比修改CPU快干的内存要快的多了。因此,如果发现有10或20个地方需要当务之急,我认为您应该完全避免一成不变,除非您可以将其模块化以用于游戏菜单等非常独立的区域。特别是游戏逻辑可能是使用不可变的好地方,因为我觉得这对于对业务规则之类的纯系统进行复杂的建模非常有用。
LegendLength

@LegendLength您在矛盾自己。
Ixx

18

我认为有些误解。纯功能程序具有状态。不同之处在于该状态的建模方式。在纯函数式编程中,状态由采取某种状态并返回下一个状态的函数操纵。然后,通过将状态传递给一系列纯函数来实现对状态的排序。

甚至全局可变状态都可以用这种方式建模。例如,在Haskell中,程序就是从一个世界到另一个世界的功能。也就是说,您传入整个Universe,程序将返回一个新的Universe。但是,实际上,您只需要传递程序实际感兴趣的部分。程序实际上返回一系列动作,这些动作作为程序运行所在的操作环境的指令。

您希望看到有关命令式编程的解释。好的,让我们来看一下使用功能语言的一些非常简单的命令式编程。

考虑以下代码:

int x = 1;
int y = x + 1;
x = x + y;
return x;

相当沼泽标准的命令式代码。没有做任何有趣的事情,但是可以进行说明。我想您会同意这里涉及到状态。x变量的值随时间变化。现在,让我们通过发明一种新的语法来稍微改变一下表示法:

let x = 1 in
let y = x + 1 in
let z = x + y in z 

加上括号以使其更清楚:

let x = 1 in (let y = x + 1 in (let z = x + y in (z)))

因此,您可以看到,状态是由一系列纯表达式构成的,这些纯表达式绑定了以下表达式的自由变量。

您会发现该模式可以对任何状态,甚至IO进行建模。


有点像单子吗?
CMCDragonkai 2014年

您是否会考虑:A在第1层是声明性的B在第2层是声明性的,它认为A是必须的。C在级别3是声明性的,它认为B是必须的。随着我们增加抽象层,它总是认为比抽象层低的语言比其本身更重要。
CMCDragonkai 2014年

14

这是在不具有可变状态的情况下编写代码的方法:不用将更改状态放入可变变量中,而是将其放入函数的参数中。而不是编写循环,而是编写递归函数。因此,例如以下命令代码:

f_imperative(y) {
  local x;
  x := e;
  while p(x, y) do
    x := g(x, y)
  return h(x, y)
}

变成以下功能代码(类似于方案的语法):

(define (f-functional y) 
  (letrec (
     (f-helper (lambda (x y)
                  (if (p x y) 
                     (f-helper (g x y) y)
                     (h x y)))))
     (f-helper e y)))

或此Haskellish代码

f_fun y = h x_final y
   where x_initial = e
         x_final   = loop x_initial
         loop x = if p x y then loop (g x y) else x

关于为什么函数式程序员喜欢这样做(您没有问过),您的程序越多部分是无状态的,就越多地将各个部分组合在一起而不会造成任何中断。无状态范式的能力本身并不在于无状态(或纯度),而在于它赋予您编写强大的,可重用的功能并将其组合的能力。

您可以在John Hughes的论文《功能编程为何如此重要》中找到一个很好的教程,其中包含许多示例。


13

做同一件事只是不同的方式。

考虑一个简单的示例,例如将数字3、5和10相加。想象一下,首先通过将3加5,然后将10加到“ 3”,然后输出当前的“ 3英寸(18)。这显然是荒谬的,但是从本质上讲,这是经常执行基于状态的命令式编程的方式。确实,您可以有许多不同的“ 3”,它们的值均为3,但有所不同。所有这些似乎都很奇怪,因为我们一直非常根深蒂固地认为数字是不可变的。

现在考虑将值设为不可变时将3、5和10相加。您将3和5相加得出另一个值8,然后将该值相加10得出另一个值18。

这些是做相同事情的等效方法。两种方法都存在所有必需的信息,但是形式不同。一方面,信息以状态形式存在,而在状态改变规则中存在。另一方面,信息以不变的数据和功能定义存在。


10

我来不及讨论,但是我想为那些在函数式编程中苦苦挣扎的人加点注意。

  1. 功能语言与命令式语言保持完全相同的状态更新,但是它们通过将更新后的状态传递给后续函数调用来做到这一点。这是沿着数字线行驶的非常简单的示例。您的状态是您当前的位置。

首先命令式方式(用伪代码)

moveTo(dest, cur):
    while (cur != dest):
         if (cur < dest):
             cur += 1
         else:
             cur -= 1
    return cur

现在的功能方式(用伪代码)。我非常依赖于三元运算符,因为我希望来自命令式背景的人能够实际读取此代码。因此,如果您不大量使用三元运算符(在必要的日子里我总是避免使用它),这就是它的工作原理。

predicate ? if-true-expression : if-false-expression

您可以通过将新的三元表达式代替false表达式来链接三元表达式

predicate1 ? if-true1-expression :
predicate2 ? if-true2-expression :
else-expression

因此,请记住这是功能版本。

moveTo(dest, cur):
    return (
        cur == dest ? return cur :
        cur < dest ? moveTo(dest, cur + 1) : 
        moveTo(dest, cur - 1)
    )

这是一个简单的例子。如果要在游戏世界中移动人们,则必须引入一些副作用,例如在屏幕上绘制对象的当前位置,并根据对象移动的速度在每次调用中引入一些延迟。但是您仍然不需要可变状态。

  1. 课程是功能语言通过使用不同的参数调用函数来“改变”状态。显然,这并没有真正改变任何变量,但这就是您获得类似效果的方式。这意味着如果您要进行函数式编程,则必须习惯于递归思考。

  2. 学习递归思考并不难,但这需要实践和工具包。他们使用递归来计算阶乘的那本“学习Java”书中的一小部分并没有减少它。您需要一个技能工具包,例如使用递归进行迭代过程(这就是为什么尾递归对于功能语言必不可少的原因),延续,不变式等。在不了解访问修饰符,接口等的情况下,您将不会进行OO编程。用于功能编程。

我的建议是做小计划者(请注意,我说“做”而不是“读”),然后在SICP中进行所有练习。完成后,您将拥有与开始时不同的大脑。


8

实际上,即使在没有可变状态的语言中,看起来像可变状态的东西也很容易。

考虑一个类型为的函数s -> (a, s)。从Haskell语法翻译过来,它表示一个函数,该函数采用“ s” 类型的一个参数并返回“ a”和“ s” 类型的一对值。如果s是我们状态的类型,则此函数将采用一个状态并返回一个新状态,并可能返回一个值(您始终可以返回“ unit” aka ()void在C / C ++中与“ a” 等效,作为“ ”类型)。如果将具有此类类型的多个函数调用链接在一起(获取从一个函数返回的状态并将其传递给下一个函数),那么您将具有“可变”状态(实际上,您在每个函数中都创建了一个新状态并放弃了旧状态。 )。

如果将可变状态想象为程序正在执行的“空间”,然后考虑时间维度,可能会更容易理解。在时刻t1,“空间”处于特定条件下(例如,某些存储位置的值为5)。在稍后的时刻t2,它处于不同的状态(例如,存储位置现在的值为10)。这些时间的每个“切片”都是一个状态,并且是不可变的(您无法及时返回以更改它们)。因此,从这个角度来看,您从带有时间箭头的完整时空(您的可变状态)转到一组时空切片(几个不可变状态),并且程序只是将每个切片视为一个值并计算每个其中一个作为应用于上一个的函数。

好吧,也许这并不容易理解:-)

将整个程序状态显式表示为一个值似乎是没有必要的,只需要创建该值就可以在下一个瞬间(刚创建一个新的瞬间)就将其丢弃。对于某些算法而言,这很自然,但是如果不是,则有另一招。除了真实状态外,您还可以使用伪造状态,该伪造状态仅是标记(我们将其称为伪造状态的类型State#)。从语言的角度来看,这种伪状态存在,并且像其他任何值一样传递,但是编译器在生成机器代码时完全忽略了它。它仅用于标记执行顺序。

例如,假设编译器为我们提供了以下功能:

readRef :: Ref a -> State# -> (a, State#)
writeRef :: Ref a -> a -> State# -> (a, State#)

从这些类似Haskell的声明进行转换,readRef会收到类似于“ a” 类型的值的指针或句柄以及伪状态,并返回a第一个参数和新的伪状态所指向的“ ” 类型的值。writeRef相似,但改为更改指向的值。

如果调用readRef然后将其传递给伪造的状态writeRef(也许与其他对中间不相关函数的调用一起传递给它;这些状态值创建函数调用的“链”),则它将返回写入的值。您可以writeRef使用相同的指针/句柄再次调用,并且它将写入相同的内存位置-但是,由于从概念上讲,它返回一个新的(伪)状态,因此(伪)状态仍然是不可更改的(已经创建了一个新状态) ”)。如果存在必须计算的真实状态变量,但是唯一存在的状态是真实硬件的完整(可变)状态,则编译器将按调用它们的顺序来调用函数。

(这些谁知道哈斯克尔会注意到我简化了很多东西和中省略一些重要的细节。对于那些谁希望看到更多的细节,看看Control.Monad.Statemtl,并在ST sIO(又名ST RealWorld)的单子。)

您可能想知道为什么以这种回旋方式进行操作(而不是简单地使用语言中的可变状态)。真正的好处是,你已经物化程序的状态。以前是隐式的(您的程序状态是全局的,允许进行类似操作的事情)现在是显式的。不接收和返回状态的功能无法对其进行修改或受到其影响;他们是“纯”的。更好的是,您可以有单独的状态线程,并具有一些类型魔术,可以将它们用于将命令式计算嵌入到一个纯粹的命令中,而不会使其变得不纯净(STHaskell中的monad是通常用于此技巧的代码;在State#我上面提到的,其实是GHC的State# s,其实现的使用STIO monads)。


7

函数式编程避免状态并强调功能。没有状态永远不会存在,尽管状态实际上可能是不可变的,或者已经融入您正在使用的体系结构中。考虑一下仅从文件系统加载文件的静态Web服务器与实现Rubik多维数据集的程序之间的区别。前者将根据旨在将请求转换为文件路径请求的功能转换为来自该文件内容的响应的功能来实现。几乎不需要任何配置即可完成任何状态(文件系统“状态”确实超出了程序的范围。无论文件处于何种状态,程序都以相同的方式工作)。但是,在后者中,您需要对多维数据集和程序实现进行建模,以了解该多维数据集上的操作如何更改其状态。


当我更加反功能时,我想知道当硬盘驱动器等易变的东西怎么可能很好。我的c#类都具有可变状态,并且可以从逻辑上模拟硬盘驱动器或任何其他设备。鉴于功能,模型与它们所建模的实际机器之间不匹配。在深入研究功能之后,我开始意识到,您所获得的好处将远远超过该问题。而且,如果有可能发明一种能够复制自身的硬盘驱动器,那么它实际上将是有用的(就像日志记录一样)。
LegendLength

5

除了其他人给出的出色答案之外,请考虑类IntegerStringJava。这些类的实例是不可变的,但这并不能使这些类无用,因为它们的实例无法更改。不变性为您提供一些安全性。您知道如果使用String或Integer实例作为的键Map,则不能更改该键。将此与DateJava中的类进行比较:

Date date = new Date();
mymap.put(date, date.toString());
// Some time later:
date.setTime(new Date().getTime());

您已默默地更改了地图中的键!使用不可变对象(例如在函数式编程中)要干净得多。更容易推断出发生了什么副作用-无!这意味着对于程序员来说更容易,对于优化器来说也更容易。


2
我了解这一点,但无法回答我的问题。请记住,计算机程序是某些现实事件或过程的模型,如果您不能更改值,那么如何为变化的模型建模?
梅森惠勒2009年

好吧,您当然可以使用Integer和String类做有用的事情。并不是说它们的不变性意味着您不能具有可变状态。
Eddie

@Mason Wheeler-通过了解事物及其状态是两个不同的“事物”。从时间A到时间B,吃豆子不变。吃豆子在哪里变化。当您从时间A移至时间B时,您将获得pacman + state ...的新组合...这是相同的pacman,但状态不同。未更改状态...其他状态。
RHSeeger

4

对于游戏等高度交互的应用程序,Functional Reactive Programming是您的朋友:如果您可以将游戏世界的属性表述为随时间变化的值(和/或事件流),那么您就准备好了!这些公式有时甚至比改变状态更自然,更有意义,例如,对于移动的球,您可以直接使用众所周知的定律x = v * t。而且更棒的是,游戏的规则写了这样的方式撰写不是面向对象的抽象更好。例如,在这种情况下,球的速度也可以是随时间变化的值,该值取决于由球的碰撞组成的事件流。有关更多具体设计注意事项,请参阅《在Elm中制作游戏》



3

这就是FORTRAN在没有COMMON块的情况下工作的方式:您将编写具有传入值和局部变量的方法。而已。

面向对象的编程将状态和行为结合在一起,但是当我1994年第一次从C ++遇到它时,这是一个新主意。

哎呀,当我是机械工程师时,我是一名功能程序员,但我不知道!


2
我不同意这是可以固定在OO上的东西。OO之前的语言鼓励耦合状态和算法。OO只是提供了更好的管理方式。
杰森·贝克

“鼓励”-也许。OO使它成为语言的明确组成部分。您可以使用C进行封装和信息隐藏,但是我想说OO语言使它变得容易得多。
duffymo

2

请记住:功能语言已经完成了图灵。因此,可以使用功能性语言来完成使用命令式语言执行的任何有用任务。归根结底,我认为混合方法尚需说些什么。像F#和Clojure(我相信其他人)这样的语言鼓励无状态设计,但在必要时允许可变性。


仅仅因为图灵的两种语言都完整,并不意味着它们可以执行相同的任务。这意味着他们可以执行相同的计算。Brainfuck已经完成了Turing,但我可以肯定它不能通过TCP堆栈进行通信。
RHSeeger

2
当然可以。给定与C相同的硬件访问权限,可以。那并不意味着它是可行的,但是存在可能性。
杰森·贝克

2

您不能使用纯有用的功能语言。总是有一定程度的可变性,您必须处理,IO是一个例子。

将功能语言视为您使用的另一种工具。它对某些事物有益,但对其他事物则无益。您提供的游戏示例可能不是使用功能语言的最佳方法,至少屏幕会处于可变状态,您无法使用FP做任何事情。使用FP解决问题的方式和解决问题的方式与使用命令式编程所习惯的方式不同。



-3

这很简单。您可以在函数式编程中使用任意数量的变量,但前提是它们是局部变量(包含在函数内部)。因此,只需将您的代码包装在函数中,在这些函数之间来回传递值(作为传递的参数和返回值)……仅此而已!

这是一个例子:

function ReadDataFromKeyboard() {
    $input_values = $_POST[];
    return $input_values;
}
function ProcessInformation($input_values) {
    if ($input_values['a'] > 10)
        return ($input_values['a'] + $input_values['b'] + 3);
    else if ($input_values['a'] > 5)
        return ($input_values['b'] * 3);
    else
        return ($input_values['b'] - $input_values['a'] - 7);
}
function DisplayToPage($data) {
    print "Based your input, the answer is: ";
    print $data;
    print "\n";
}

/* begin: */
DisplayToPage (
    ProcessInformation (
        GetDataFromKeyboard()
    )
);

约翰,这是什么语言?
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.