【问题标题】:What is the purpose of the state monad?状态单子的目的是什么?
【发布时间】:2025-12-05 20:35:02
【问题描述】:

我是一名 JavaScript 开发人员,正在提升我的函数式编程技能。我最近在管理状态方面遇到了困难。在寻找解决方案时,我在各种文章和视频中偶然发现了 state monad,但我很难理解它。我想知道是不是因为我希望它不是。

我要解决的问题

在 Web 客户端中,我从后端获取资源。为了避免不必要的流量,我在客户端创建了一个简单的缓存,其中包含已经获取的数据。缓存是我的状态。我希望我的几个模块能够保存对缓存的引用并查询它的当前状态,该状态可能已被另一个模块修改。

这在 javascript 中当然不是问题,因为可以改变状态,但我想了解更多关于函数式编程的知识,我希望 state monad 能帮助我。

我的期望

我曾假设我可以做这样的事情:

var state = State.of(1);
map(add(1), state);
state.evalState() // => 2 

这显然行不通。状态始终为 1。

我的问题

我对 state monad 的假设是错误的,还是我只是不正确地使用它?

我意识到我可以做到:

var state = State.of(1);
var newState = map(add(1), state);

... 和 newState 将是状态 2。但是在这里我并没有真正看到 state monad 的使用,因为我必须创建一个新实例才能改变值。在我看来,这似乎是值不可变的函数式编程中总是做的事情。

【问题讨论】:

    标签: functional-programming monads state-monad


    【解决方案1】:

    state monad 的目的是隐藏函数之间的状态传递。

    举个例子:

    方法 A 和 B 需要使用一些状态并对其进行变异,而 B 需要使用 A 变异的状态。在具有不可变数据的函数式语言中,这是不可能的。

    所做的是:一个初始状态连同它需要的参数一起传递给 A,然后 A 返回一个结果和一个“修改后的”状态——实际上是一个新值,因为原始状态没有改变.这个“新”状态(也可能是结果)通过其所需的参数传递给 B,然后 B 返回 结果和它(可能)修改的状态。

    显式传递这个状态是一个 PITA,所以 State monad 将它隐藏在它的 monadic 掩护下,允许需要访问状态的方法通过 getset monadic 方法来获取它。

    为了使用有状态计算 A 和 B,我们将它们组合成一个联合有状态计算,并为该联合提供一个开始状态(和参数)以运行,它返回最终“修改”状态和结果(运行后通过 A、B 和其他任何组成的东西)。

    根据您的描述,在我看来,您正在寻找更多类似于 actor model of concurrency 的内容,其中状态在演员中管理,其余代码通过该演员与之交互,检索(非可变版本)它或告诉它通过消息进行修改。在不可变语言(如 Erlang)中,actor 阻塞等待消息,然后在收到消息时处理它,然后通过(尾)递归循环;他们将任何修改后的状态传递给递归调用,这就是状态被“修改”的方式。

    不过,正如您所说,因为您使用的是 JavaScript,所以这不是什么大问题。

    【讨论】:

    • 感谢您的描述。我认为让我感到困惑的是,您所描述的 A 接收状态并将其结果传递给我听起来就像正常的函数组合。但无论如何,我想状态不是我的问题的解决方案。
    • 它在概念上非常接近函数组合,但你可以认为事情发生在两个层面:一元函数(例如 A)将像其他函数一样具有正常的args -> result 层,但在同时还有另一层state -> state。状态通过每个函数在其自己的组合级别上进行线程化;在常规级别上,函数可以返回一个结果,或者更像是一个过程,它们只是产生副作用(改变底层状态)并返回一个 void 或 unit 或任何它在特定语言中的内容。
    • @LudwigMagnusson 还有其他几个具有这种组合性质的 monad:Either 和 Maybe monad。实际上,这些 monad 可以导致组合链提前退出 - 在 Either monad 的情况下,以某种其他类型的结果退出(例如,带有错误消息的字符串)。你可能会觉得this tutorial series 很有趣,尤其是part two
    【解决方案2】:

    我试图从 Javascript 开发人员的角度回答您的问题,因为我相信这是您的问题的原因。也许您可以在标题和标签中指定术语 Javascript。

    将概念从 Haskell 转移到 Javascript 基本上是一件好事,因为 Haskell 是一种非常成熟的纯函数式语言。但是,它可能会导致混乱,就像 state monad 一样。

    例如,maybe monad 很容易理解,因为它处理两种语言都面临的一个问题:不返回值可能会出错的计算(JavaScript 中的null/undefined)。 Maybe 使开发人员免于在整个代码中分散null 检查。

    在 state monad 的情况下,情况有点不同。在 Haskell 中,需要 state monad 来组合函数,这些函数共享可变状态,而无需传递此状态。状态是不属于所涉及函数的参数的一个或多个变量。在 Javascript 中,您可以执行以下操作:

    var stack = {
      store: [],
      push: function push(element) { this.store.push(element); return this; },
      pop: function pop() { return this.store.pop(); }
    }
    
    console.log(stack.push(1).push(2).push(3).pop()); // 3 (return value of stateful computation)
    console.log(stack.store); // [1, 2] (mutated, global state)
    

    这是所需的有状态计算,store 不必在方法之间传递。乍一看,没有理由在 Javascript 中使用 state monad。但由于 store 是公开可访问的,pushpop 会改变全局状态。改变全局状态是个坏主意。这个问题可以通过多种方式解决,其中之一就是 state monad。

    以下简化示例将堆栈实现为状态单子:

    function chain(mv, mf) {
      return function (state) {
        var r = mv(state);
        return mf(r.value)(r.state);
      };
    }
    
    function of(x) {
      return function (state) {
        return {value: x, state: state};
      };
    }
    
    function push(element) {
      return function (stack) {
        return of(null)(stack.concat([element]));
      };
    }
    
    function pop() {
      return function (stack) {
        return of(stack[stack.length - 1])(stack.slice(0, -1));
      };
    }
    
    function runStack(seq, stack) { return seq(stack); }
    function evalStack(seq, stack) { return seq(stack).value; }
    function execStack(seq, stack) { return seq(stack).state; }
    function add(x, y) { return x + y; }
    
    // stateful computation is not completely evaluated (lazy evaluation)
    // no state variables are passed around
    var computation = chain(pop(), function (x) {
      if (x < 4) {
        return chain(push(4), function () {
          return chain(push(5), function () {
            return chain(pop(), function (y) {
              return of(add(x, y));
            });
          });
        });
      } else {
        return chain(pop(), function (y) {
          return of(add(x, y));
        });
      }
    });
    
    var stack1 = [1, 2, 3],
      stack2 = [1, 4, 5];
    
    console.log(runStack(computation, stack1)); // Object {value: 8, state: Array[3]}
    console.log(runStack(computation, stack2)); // Object {value: 9, state: Array[1]}
    
    // the return values of the stateful computations
    console.log(evalStack(computation, stack1)); // 8
    console.log(evalStack(computation, stack2)); // 9
    
    // the shared state within the computation has changed
    console.log(execStack(computation, stack1)); // [1, 2, 4]
    console.log(execStack(computation, stack2)); // [1]
    
    // no globale state has changed
    cosole.log(stack1); // [1, 2, 3]
    cosole.log(stack2); // [1, 4, 5]
    

    可以避免嵌套函数调用。为简单起见,我省略了此功能。

    在 Javascript 中没有任何问题可以单独使用 state monad 来解决。并且很难理解像 state monad 这样概括的东西,它解决了所用语言中看似不存在的问题。它的使用只是个人喜好问题。

    【讨论】:

    • 只是为了明确一点:你不需要使用 Haskell 中的 State Monad 来改变状态,实际上你并没有真正 mutate i> 任何地方的状态 - 基本上这个想法是将不断变化的 state 作为附加参数传递(使其明确),并且 State-Monad 抽象出这个参数传递 - 当然有方法可以真的变异的东西,但这通常在IOST-Monad中完成
    • @Carsten:谢谢,这有帮助。我相应地更新了我的答案
    【解决方案3】:

    它确实像你的第二个描述一样工作,返回一个新的不可变状态。但是,如果您这样称呼它,它并不是特别有用。它派上用场的地方是,如果您有一堆要调用的函数,每个函数都获取从上一步返回的状态并返回一个新状态,可能还有另一个值。

    使它成为一个 monad 基本上允许您指定一个仅包含要执行的函数名称的列表,而不是一遍又一遍地重复 newState = f(initialState); newNewState = g(newState); finalState = h(newNewState);。 Haskell 有一个称为 do-notation 的内置符号来精确地做到这一点。您如何在 JavaScript 中完成它取决于您使用的函数库,但在其最简单的形式(没有任何中间结果绑定)中,它可能类似于 finalState = do([f,g,h], initialState)

    换句话说,状态单子并没有神奇地使不变性看起来像可变性,但它可以在某些情况下简化中间状态的跟踪。

    【讨论】:

    • 我使用 ramda 的 compose 函数来完成您所描述的内容,但我无法真正看到状态如何改变游戏。不过感谢您的澄清。
    • 这也是我的想法,你可以编写一个等待状态对象的柯里化函数,因为我是 fp 的新手,我不确定这种方法与 compose 相比有什么好处
    【解决方案4】:

    状态无处不在。在类中,它可能是其属性的值。在程序中,它可能是变量的值。在允许可变性的 javascript 甚至 java 之类的语言中,我们将状态作为参数传递给 mutating 函数。然而,在 Haskell 和 Scala 等语言中,它们不喜欢 mutation(称为副作用或不纯),新的状态(带有更新)被显式返回,然后传递给它的消费者.为了隐藏这种显式的状态传递和返回,Haskell(和 Scala)有这个 State Monad 的概念。我在https://lakshmirajagopalan.github.io/state-monad-in-scala/写了一篇同样的文章

    【讨论】:

      最近更新 更多