Option 和 OptionT 有什么区别?

What is the difference between Option and OptionT?

fp-ts 中有两个模块:

  1. Option
  2. OptionT

正如 Code Conventions 章节所述

However usually it means Transformer like in “monad transformers” (e.g. OptionT, EitherT, ReaderT, StateT)

那么什么是变形金刚?我怎么知道要使用什么导入?

欢迎提供示例。

Monad Transformer 是一般函数式编程的概念。我会遵从维基百科的定义,因为它看起来非常精确:

In functional programming, a monad transformer is a type constructor which takes a monad as an argument and returns a monad as a result.

Monad transformers can be used to compose features encapsulated by monads – such as state, exception handling, and I/O – in a modular way. Typically, a monad transformer is created by generalising an existing monad; applying the resulting monad transformer to the identity monad yields a monad which is equivalent to the original monad (ignoring any necessary boxing and unboxing).

-- Wikipedia


为了帮助说明为什么这可能是一组很好的实用程序来创建,让我们看一下 OptionT 模块中的 match 定义。

// It is overloaded a few time to support Functors with multiple type parameters
// but this is the simplest
export declare function match<F>(
  F: Functor<F>
): <B, A>(onNone: () => B, onSome: (a: A) => B) => (ma: HKT<F, Option<A>>) => HKT<F, B>

如果您有一个 Functor 并且希望能够处理该仿函数一次持有一个 Option 值的情况,您将使用此函数。

例如,如果您的 FunctorArray,那么您可以使用 match,例如

import * as A from 'fp-ts/Array';
import * as O from 'fp-ts/Option';
import { match } from 'fp-ts/OptionT';
import { pipe } from 'fp-ts/function';

const defaultTo0 = match(
  A.Functor,
)(
  () => 0,
  (a: number) => a,
);

const sum = pipe(
  [O.some(1), O.some(2), O.none, O.some(3)],
  defaultTo0,
  A.reduce(0, (acc, c) => acc + c),
);

console.log(sum); // 6

在这种情况下,我们使用选项转换器模块中的 match 函数来创建一个将 Array<Option<number>> 转换为 Array<number> 的助手。 Monad Transformers 是一个非常广泛的主题,因此很难用比维基百科文章所说的更明确的术语说“这就是你使用它的时候”。希望这个例子提供了一个小例子,说明它如何使管理多个 monadic 层变得不那么乏味?

让我们看看Option and OptionT是什么

export type Option<A> = None | Some<A>
/** @deprecated */
export interface OptionT<M, A> extends HKT<M, Option<A>> {}

Option是我们熟悉的基础monad,代表一个Some<A>None.

OptionT<M, A> 等同于 HKT<M, Option<A>>。这个接口实际上是 deprecated ——我稍后会解释这个。但是,您仍然可以在不使用 OptionT 类型的情况下使用 OptionT 模块中的辅助函数。

你可以这样想 OptionT(遗憾的是,由于 TypeScript 缺少更高种类的类型,它无法编译):

type OptionT<M, A> = M<Option<A>>

OptionT 是 monad 转换器的一个例子。 monad transformer 是一种将多个 monad 组合成一个新的 monad 的方法,因此我们可以使用 chain(和其他实用函数)。

例子

让我们看一个(稍微做作的)例子。

import * as Console from 'fp-ts/Console'
import * as IO from 'fp-ts/IO'
import * as O from 'fp-ts/Option'
import {pipe} from 'fp-ts/function'

type IOOption<A> = IO.IO<O.Option<A>> // our new monad
// You could think of IOOption<A> as OptionT<IO, A>

const getNumber: IOOption<number> = pipe(
  Console.log('getting number'),
  IO.map(() => O.some(42))
)

/** Returns None when the number is not divisible by 2. */
const half = (number: number): IOOption<number> =>
  pipe(
    Console.log('halving number'),
    IO.map(() => (number % 2 ? O.none : O.some(number / 2)))
  )

我们如何把halfgetNumber放在一起?最初,您可能会想到做这样的事情:

const bad: IOOption<number> = pipe(getNumber, IO.chain(O.chain(half)))
//                                                             ~~~~
// Argument of type '(number: number) => IOOption<number>' is not
// assignable to parameter of type '(a: number) => Option<unknown>'.

然而,这是行不通的。 O.chain 接受 returns 一个 Option 而不是 IOOption 的函数。此外,IO.chain 接受 returns 和 IO 的函数,但 O.chain returns 接受 returns 和 Option 的函数。 (有关更多信息,您可以阅读如何 monads don’t compose。)

正确的做法是:

const good = pipe(
  getNumber,
  IO.chain(O.fold(
    // If we get a None, return IO None
    () => IO.of(O.none),
    // If we get a Some(number), return half(number)
    half
  ))
)

对于这个小例子来说这似乎很好,但是你可以想象当你将越来越多的函数链接在一起时这会变得有点笨拙:

pipe(
  foo,
  IO.chain(O.fold(() => IO.of(O.none), bar)),
  IO.chain(O.fold(() => IO.of(O.none), baz)),
  IO.chain(O.fold(() => IO.of(O.none), qux))
)

幸运的是,我们有 OptionT 模块。使用此模块中的 chain,我们可以删除 IO.chain(O.fold(() => IO.of(O.none), ...)) 样板文件:

import * as OT from 'fp-ts/lib/OptionT'

const better = pipe(getNumber, OT.chain(IO.Monad)(half))

const ioOptionChain = OT.chain(IO.Monad)
const alsoBetter = pipe(getNumber, ioOptionChain(half))

OT.chain的类型是这样的:

// chain :: Monad m => (a -> m (Option b)) -> m (Option a) -> m (Option b)
export declare function chain<M>(
  M: Monad<M>
): <A, B>(f: (a: A) => HKT<M, Option<B>>) => (ma: HKT<M, Option<A>>) => HKT<M, Option<B>>
// a bunch of other overloads omitted for brevity

您可能已经注意到我们在 good 中使用的方法可以推广到任何 monad M:

M.chain(O.fold(() => M.of(O.none), fn))

这就是 OT.chain 接受 M: Monad<M> 的原因:一个 monad 实例,例如 IO.MonadTask.MonadEither.Monad

我使用 Option 还是 OptionT

如果您只处理 Option,请使用 Option。使用 OptionT 如果 Option 包含在其他类型中,例如 IOTaskArrayEither

为什么 OptionT 被弃用

回想一下 OptionT<M, A> 等同于 HKT<M, Option<A>>。虽然您会看到 HKT 在某些实用函数的重载中弹出,但您可能不会在自己的类型中使用它。例如,我们的 IOOption<A> 不是 HKT<'IO', A> 而是等同于 Kind<'IO', Option<A>>.

但是,Kind 仅适用于具有 1 个类型参数的 monad。对于更多类型参数,有 Kind2(例如 Either)、Kind3(例如 ReaderEither)和 Kind4(例如 StateReaderTaskEither ).

这就是 OT.chain 和其他类似函数有这么多重载的原因:

export declare function chain<M extends URIS4>(M: Monad4<M>): <A, S, R, E, B>(f: (a: A) => Kind4<M, S, R, E, Option<B>>) => (ma: Kind4<M, S, R, E, Option<A>>) => Kind4<M, S, R, E, Option<B>>
export declare function chain<M extends URIS3>(M: Monad3<M>): <A, R, E, B>(f: (a: A) => Kind3<M, R, E, Option<B>>) => (ma: Kind3<M, R, E, Option<A>>) => Kind3<M, R, E, Option<B>>
export declare function chain<M extends URIS3, E>(M: Monad3C<M, E>): <A, R, B>(f: (a: A) => Kind3<M, R, E, Option<B>>) => (ma: Kind3<M, R, E, Option<A>>) => Kind3<M, R, E, Option<B>>
export declare function chain<M extends URIS2>(M: Monad2<M>): <A, E, B>(f: (a: A) => Kind2<M, E, Option<B>>) => (ma: Kind2<M, E, Option<A>>) => Kind2<M, E, Option<B>>
export declare function chain<M extends URIS2, E>(M: Monad2C<M, E>): <A, B>(f: (a: A) => Kind2<M, E, Option<B>>) => (ma: Kind2<M, E, Option<A>>) => Kind2<M, E, Option<B>>
export declare function chain<M extends URIS>(M: Monad1<M>): <A, B>(f: (a: A) => Kind<M, Option<B>>) => (ma: Kind<M, Option<A>>) => Kind<M, Option<B>>
export declare function chain<M>(M: Monad<M>): <A, B>(f: (a: A) => HKT<M, Option<B>>) => (ma: HKT<M, Option<A>>) => HKT<M, Option<B>>

我们在OT.chain(IO.Monad)中实际使用的重载是其中带有Kind的重载(倒数第二个)。

这就是为什么使用 OptionT 类型没有多大意义,因为它只能用于 HKT 的最后一个重载。


如果您想了解有关 monad 转换器的更多信息:

即使它们大约是 Haskell,相同的原则也适用于 fp-ts。