Effect.succeed 和 Effect.sync 有什么区别?

Effect.succeed(Date.now()) 无论运行多少次,拿到的都是同一个时间戳。

区别在求值时机:Effect.succeed(f()) 会立即调用 f,而 Effect.sync(f) 把调用推迟到 Effect 被运行的时候。 需要每次运行都重新计算,写成 Effect.sync(() => Date.now())。

原因在于 Effect 是一个值,一份「要做什么」的描述,创建时不执行,被运行时才执行,详见 Effect 和 Promise 有什么区别。惰性只在描述内部成立:参数表达式是普通的 TypeScript,在传进构造函数之前就已经求值了。

为什么 Effect.succeed(Date.now()) 总是同一个值?

Date.now() 是 succeed 的参数,在构造 Effect 时就已经被求值,succeed 拿到的只是一个已经算好的数字。之后每次运行,拿到的都是这个数字。

const fixed = Effect.succeed(Date.now())
const fresh = Effect.sync(() => Date.now())

fresh 里 Date.now() 被包在函数里,不会在构造时调用;每次运行才调用一次。

副作用应该写在哪里?

副作用必须放在 Effect.sync、Effect.tryPromise 这类构造函数的回调里,这样才是惰性的。写在 Effect.gen 外面的副作用,会在定义的那一刻就发生,Effect 管不到它。

把 Effect 存起来再用,会重复执行副作用吗?

会,而且这是特性不是 bug:同一个 Effect 每次运行都会重新执行。

const hello = Effect.sync(() => {
  console.log("hello")
})

const twice = Effect.gen(function* () {
  yield* hello
  yield* hello
})

运行 twice 会打印两次。想要「只算一次」,需要显式的缓存,而不是指望 Effect 自己记住结果。

本文的示例和行为描述没有逐条重新运行,写进项目前建议自己跑一遍确认。