在函数式编程中,为什么 IO 的 join 方法要运行 unsafePerformIO 两次?



在DrBoolean的Gitbook中,有几个例子解释了monad,也许:

Maybe.prototype.join = function() {
return this.isNothing() ? Maybe.of(null) : this.__value;
}

对于 IO:

IO.prototype.join = function() {
var thiz = this;
return new IO(function() {
return thiz.unsafePerformIO().unsafePerformIO();
});
};

我想知道为什么 IO 应该运行 unsafePerformIO 两次以返回新的 IO 而不仅仅是return this.unsafePerformIO()

在我这么说之前没有IO

对于 IO,重要的是我们不要执行任何 IO,直到需要 – 在下面的示例中,请特别注意输出行的顺序

// IO
const IO = function (f) {
this.unsafePerformIO = f
}
IO.of = function (x) {
return new IO(() => x)
}
IO.prototype.join = function () {
return this.unsafePerformIO()
}
// your main program
const main = function (m) {
console.log('you should not see anything above this line')
console.log('program result is:', m.unsafePerformIO())
}
// IO (IO (something))
const m = new IO(() => {
console.log('joining...')
return IO.of(5)
})
// run it
main(m.join())

上面,joining...出现的时间比我们预期/期望的要早 - 现在将其与正确的IO.join实现进行比较 -所有效果都推迟到在最外层的IO上调用unsafePerformIO


再次装箱,拆箱两次

通常,所有 IO 操作都会在延迟计算周围添加一个新框。具体来说join,我们仍然需要添加一个新框,但操作是拆箱两次,所以我们仍然有效地从嵌套的 2 级下降到 1 级。

// IO
const IO = function (f) {
this.unsafePerformIO = f
}
IO.of = function (x) {
return new IO(() => x)
}
IO.prototype.join = function () {
return new IO(() => this.unsafePerformIO().unsafePerformIO())
}
// your main program
const main = function (m) {
console.log('you should not see anything above this line')
console.log('program result is:', m.unsafePerformIO())
}
// IO (IO (something))
const m = new IO(() => {
console.log('joining...')
return IO.of(5)
})
// run it
main(m.join())


不仅仅是 IO

可以说,这种join的再次开箱方法也适用于其他monad

function Maybe (x) {
this.value = x
}
Maybe.of = function (x) {
return new Maybe(x)
}
Maybe.prototype.join = function () {
// assumes that this.value is a Maybe
// but what if it's not?
return this.value;
}
Maybe.prototype.toString = function () {
return `Maybe(${this.value})`
}
const m = Maybe.of(Maybe.of(5))
console.log("m               == %s", m)
console.log("m.join()        == %s", m.join())
// hmm... now it seems `.join` can return a non-Maybe??
console.log("m.join().join() == %s", m.join().join())

上面,它看起来Maybe.join有时会返回一个 Maybe,而其他时候它可以简单地返回框值。因为它不能保证返回一个 May,所以它更难依赖它的行为

现在,将其与下面的"再次开箱两次"方法进行比较

function Maybe (x) {
this.value = x
}
Maybe.of = function (x) {
return new Maybe(x)
}
Maybe.prototype.join = function () {
// box again, unbox twice
// guaranteed to return a Maybe
return Maybe.of(this.value.value)
}
Maybe.prototype.toString = function () {
return `Maybe(${this.value})`
}
const m = Maybe.of(Maybe.of(5))
console.log("m               == %s", m)
// this still works as intended
console.log("m.join()        == %s", m.join())
// here join still returns a Maybe as expected,
// but the inner value `undefined` reveals a different kind of problem
console.log("m.join().join() == %s", m.join().join())


弱类型JavaScript

在上面的例子中,我们的Maybe(Maybe(Number))转换为Maybe(Maybe(undefined))这将导致强类型语言的错误。但是,在 JavaScript 的情况下,在您尝试使用您实际期望5undefined之前,此类错误不会很明显——这是一种不同类型的问题,但我个人更喜欢已知的协域(返回类型)而不是我稍后必须进行类型检查的协域。

当然,我们可以通过在 join 本身内部进行类型检查来解决此问题,但现在 May 是不纯的,可能会在运行时抛出错误。

Maybe.prototype.join = function () {
if (this.value instanceof Maybe)
return this.value
else
throw TypeError ('non-Maybe cannot be joined')
}

可悲的是,这就是JavaScript在函数式编程的某些方面崩溃的地方。这里的每个Maybe.join实现都有权衡,因此您必须选择最适合您的实现。


某种幂等性

也许你甚至可以像幂等函数一样编写Maybe.join;如果可以的话,它会加入,否则它只会返回自己——现在你得到了保证的Maybe返回类型,并且不可能出现运行时错误

Maybe.prototype.join = function () {
if (this.value instanceof Maybe)
return this.value
else
return this
}

但是,以下程序现在已通过此实现进行验证

// should this be allowed?
Maybe.of(Maybe.of(5)).join().join().join().join().join() // => Maybe(5)

取舍,取舍,取舍。选择你的毒药或选择PureScript ^_^

最新更新