state monad 的目的是什么?

What is the purpose of the state monad?

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

我正在尝试解决的问题

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

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

我所期待的

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

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

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

我的问题

我对状态 monad 的假设是错误的,还是我只是错误地使用了它?

我意识到我可以做到:

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

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

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

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

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

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

举个例子:

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

取而代之的是:初始状态连同它需要的参数一起传递给 A,A returns 结果和 "modified" 状态——实际上是一个新值,因为原来的没有改变。这个 "new" 状态(也可能是结果)连同其所需的参数被传递给 B,并且 B returns its 结果和它的状态(可能有) 已修改。

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

为了使用有状态计算 A 和 B,我们将它们组合在一起成为一个综合有状态计算,并为该综合体提供 运行 的起始状态(和参数),它 returns a最终 "modified" 状态和结果(在 运行 通过 A、B 和任何其他组成的东西之后)。

从你的描述来看,在我看来你正在寻找更多类似 actor model of concurrency 的东西,其中状态在 actor 中管理,其余代码与之交互通过它,检索它(的非可变版本)或告诉它通过消息进行修改。在不可变语言(如 Erlang)中,actors 阻塞等待消息,然后在消息进来时处理消息,然后通过(尾)递归循环;他们将任何修改后的状态传递给递归调用,这就是状态如何获得 "modified".

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

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

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

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

在状态 monad 的情况下,情况有点不同。在 Haskell 中,需要状态 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 中使用状态 monad。但是由于 store 是可公开访问的,因此 pushpop 会改变全局状态。改变全局状态是个坏主意。这个问题可以通过多种方式解决,其中之一就是state monad。

以下简化示例将堆栈实现为状态 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 中没有任何问题可以单独使用状态 monad 解决。理解像 state monad 这样概括的东西要困难得多,它解决了所用语言中看似 non-existing 的问题。它的使用仅是个人喜好问题。

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

上写了一篇关于相同内容的文章