在Typescript中,Maybe Monad的链式方法的正确实现是什么?

时间:2018-03-26 11:02:10

标签: typescript functional-programming monads

我正在使用https://github.com/fantasyland/fantasy-land中的精彩规范,在Typescript中创建一个简单的Monad库。

此处创作者建议三个一元法则

  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)) associativity
  4. 所以我实施了法律和这个经典实现的测试。

    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 monad的Maybe情况下,左侧身份规则未通过测试。事实上:

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

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

    所以问题:

    • 为什么会这样?
    • 我错过了什么吗?
    • x => Some(x+2)的{​​{1}}是None的有效参数,或者链只应接受返回None的函数吗?
    • 如何纠正?
    • 我是否应该同时考虑NaN(这可能会将除以零的效率和其他奇怪的情况扩展为undefined + 2)?

    修改

    • 这种异常似乎也存在于功能导向语言中。我尝试使用原生Option类型在 Scala here中实施类似的案例。

2 个答案:

答案 0 :(得分:1)

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

您的Some<T>功能会通过检查isSomenull来违反此原则。要遵守monad法律,undefined函数应始终返回of的实例,而Some函数应仅检查chainthis的实例}或Some

None函数中,chain个实例可以无条件地使用其内部值调用continuation函数。 Some个实例始终返回None<T>,因此根本不需要包含值。

将这些放在一起,您的实现将类似于:

None

答案 1 :(得分:0)

如果您正在做这个项目以了解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编译器不会喜欢带有严格的null检查的代码(但可能不是严格的null检查?),并且缺少很多功能,例如ap,concat等,但它表明在ts很简单。