【问题标题】:What is the correct implementation of the chain method of a Maybe Monad in Typescript?Typescript中Maybe Monad的chain方法的正确实现是什么?
【发布时间】:2018-09-04 12:08:48
【问题描述】:

我在 Typescript 中创建一个简单的 Monad 库只是为了好玩,遵循 https://github.com/fantasyland/fantasy-land 的精彩规范。

在这里,创作者提出了三个一元法则:

  1. M.of(a).chain(f) 等价于 f(a) (左身份)
  2. m.chain(M.of) 等价于m(正确的身份)
  3. m.chain(f).chain(g) 等价于 m.chain(x => f(x).chain(g)) (关联性)

所以我实施了法律测试和这个经典实施。

import { setoid, functor, apply } from './common';

const isEmpty = (value: any) => value === null || value === undefined;

const chain = function<T, U>(fn: (val: T) => IMonad<U>) {  
  return this.isNone() ? this : fn(this.lift());
};

class Some<T> implements IMonad<T> {
  private _value: T;

  constructor(value: T) {
    this._value = value;
  }

  lift = () => this._value;
  of = (value: T) => new Some(value);
  map = functor;
  ap = apply;
  flatMap = chain;
  equals = setoid;
  isSome = () => true
  isNone = () => false
}

class None implements IMonad<any> {
  constructor() {}

  lift = () => { throw "Cannot get a value from None" };
  of = (value: any) => new None();
  map = functor;
  ap = apply;
  flatMap = chain;
  equals = setoid;
  isSome = () => false;
  isNone = () => true;
}


class MaybeStatic implements IMonadStatic<any> {
  of = (value:any) => !isEmpty(value) ? new Some(value) : new None();
}

const maybeStatic = new MaybeStatic();

export { maybeStatic as Maybe, Some, None, MaybeStatic };

运行测试我看到在None 的情况下Maybe monad left identity 规则没有通过测试。事实上:

Maybe.of(null).chain(x => Some(x+2)) !== (null + 2)

在这种情况下,Js/Ts 允许操作 null + 2 的事实似乎打破了第一条单子规则(其他语言可能不允许此操作)。我检查了很多 Maybe monad 的 javascript 和 typescript 实现,他们似乎完全忽略了这个问题。

所以问题:

  • 为什么会这样?
  • 我错过了什么吗?
  • x =&gt; Some(x+2) 是 None 的有效参数还是应该只接受返回 None 的函数?
  • 如何纠正?
  • 我是否也应该考虑NaN(这也可以扩展除以零和其他奇怪情况(如undefined + 2)的可能效率?

编辑

  • 这种异常似乎也出现在面向函数的语言中。我尝试使用原生 Option 类型在 Scala here 中实现类似的案例。

【问题讨论】:

  • 您是否使用null/undefined 值来编码None 大小写?您应该使用显式标志并忽略该值,以便将chain 实现为return this._hasValue ? fn(this._value) : this。
  • 请发布您的完整代码,包括您如何实现of、lift 和None 案例
  • "x =&gt; Some(x+2) 是 chain 的有效参数吗?" - 是的,当然,它的类型可能是 int -&gt; Maybe&lt;int&gt;。但是您的chain 方法需要适用于任意 类型,例如double -&gt; Maybe&lt;string&gt;(其中双精度值可以是NaN)或null -&gt; Maybe&lt;object&gt;。
  • 谢谢,我用Maybe Monad的完整实现编辑了这篇文章。
  • Maybe.of 必须是 Some 才能不违反法律。当然,处理 Maybes 既可以处理某些事情,也可以处理任何事情,但构造始终是明确的。 of 表示“包装这个值”。 None 不包含任何内容。您可能希望实现类似 Maybe.fromNullable 的东西作为辅助方法,但这与 monad 无关。

标签: typescript functional-programming monads


【解决方案1】:

Maybe&lt;T&gt; 类型(或任何泛型类型)应避免检查其包含的值,并且应仅基于构造函数定义的结构(在本例中为 None&lt;T&gt; 和 Some&lt;T&gt; 类)进行操作。

您的isSome 函数通过检查null 和undefined 违反了这一原则。为了遵守 monad 法则,您的 of 函数应始终返回 Some 的实例,而您的 chain 函数应仅检查 this 是 Some 或 None 的实例。

在您的chain 函数中,Some 实例可以使用其内部值无条件地调用延续函数。 None&lt;T&gt; 实例总是返回 None,因此根本不需要包含值。

将这些放在一起,您的实现将如下所示:

class Some<T> implements IMonad<T> {
  private _value: T;

  constructor(value: T) {
    this._value = value;
  }

  of = (value: any) => new Some(value);
  chain = function<U>(fn: T => IMonad<U>) { return fn(this._value); };
  isSome = () => true;
  isNone = () => false;
}

class None<T> implements IMonad<T> {
  chain = function<U>(fn: T => IMonad<U>) { return new None<U>(); };
  isSome = () => false;
  isNone = () => true;
}

class MaybeStatic implements IMonadStatic<any> {
    of =(value:any) => new Some(value);
}

【讨论】:

  • 你的代码让我很困惑。在您的帖子中,您谈到了检查 monad 是否是链方法中的 Some,但没有任何内容。这是一个错字吗?其次,如果Maybe.of 总是返回Some,则Maybe.of(null).IsSome() 方法返回true。这对最终用户来说似乎有点令人困惑。
  • @DavideMelfi - 在您将其余代码添加到问题之前,我发布了该内容。由于None 没有值,因此无法实现lift 函数,因此我无法保留您的chain 实现。将它移动到每个子类似乎是最简单的选择。不过,我不是打字稿程序员,如果这显着改变了含义,我深表歉意。
  • 至于它会让用户感到困惑,如果您认为Maybe&lt;T&gt; 是null 的替代品,但它只是一个表示零或一个项目的结构。这意味着Some(null) 的意义与列表[null] 的意义相同。您可能需要一个将null 和undefined 提升到None 的函数,但您不能为此使用of,因为单子定律要求您保持与chain 的关系。
  • 您可能想查看与您的结构类似的scala Option 类型。 chain 是 flatMap 和 Maybe.of 是 Some 构造函数。 Option.apply 的行为类似于您当前的 chain 实现。
  • 感谢您的建议和 Scala 链接。我将尝试按照它们更改我的实现,看看它是否符合单子规则。
【解决方案2】:

如果您正在做这个项目来了解 monad,那么 javascript 可能会导致与它的动态类型的额外混淆。如果你这样做是因为你想使用 monad 来编写软件,那么你可能会过度思考 monad 法则。在任何情况下,等价物的定义都没有 javascript 的 == 严格。这是可能在 ts 中的一个(不完整)实现:

class MaybeInstance<T> {
    private isSome: boolean;
    private val: T;
    constructor(isSome: boolean, val: T) {
        this.isSome = isSome;
        this.val = val;
    }
    orJust(t: T) {
        return this.isSome
            ? this.val
            : t;
    }
    map<U>(fn: (t: T) => U): Maybe<U> {
        return new MaybeInstance<U>(this.isSome,
            this.isSome
                ? fn(this.val)
                : null);
    }
    chain<U>(fn: (t: T) => Maybe<U>): Maybe<U> {
        return this.isSome
            ? fn(this.val)
            : new MaybeInstance<U>(false, null);
    }
    isEquivalent<U>(mU: Maybe<U>) {
        return this.isSome === mU.isSome
            && typeof this.val === typeof mU.val;
    }
}

export type Maybe<T> = MaybeInstance<T>;

export module Maybe {

    export const some: <T>(val: T) => Maybe<T> =
        (val) => new MaybeInstance(true, val);

    export const none: () => Maybe<any> =
        () => new MaybeInstance(false, null);

    export const fromUndef: <T>(val: T | undefined) => Maybe<T> =
        (val) => !!val && (val as any) !== NaN
            ? some(val)
            : none();

}

const a = 1;
const f = (x: number) => Maybe.fromUndef(100 / x);
const g = (x: number) => Maybe.fromUndef(x).map(String);

const mA = Maybe.some(a);

console.log(
    // M.of(a).chain(f) is equivalent to f(a) (left identity)
    mA.chain(f).isEquivalent(f(a)),
    // m.chain(M.of) is equivalent to m (right identity)
    mA.chain(Maybe.some).isEquivalent(mA),
    // m.chain(f).chain(g) is equivalent to m.chain(x => f(x).chain(g)) (associativity)
    mA.chain(f).chain(g).isEquivalent(mA.chain((x) => f(x).chain(g))),
);

ts 编译器不喜欢这个带有严格空值检查的代码(但可能不是一个足够严格的空值检查吗?)并且它缺少很多函数,如 ap、concat 等,但它表明在ts 很简单。

【讨论】:

    猜你喜欢
    • 2011-11-06
    • 2018-08-12
    • 1970-01-01
    • 2011-05-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多