【问题标题】:How to use the Trampoline type as the base monad of a transformer more efficiently?如何更有效地使用 Trampoline 类型作为变压器的基本单子?
【发布时间】:2021-02-27 03:51:28
【问题描述】:

我有一个数组转换器类型,它展示了交错的效果层,以确保合法的效果实现。您可以从类型的of 操作const arrOfT = of => x => of([of(x)]) 轻松读取结构。

该类型实现了一个有效的折叠作为它的基本操作。我使用左折叠,因为底层数组类型本质上是严格的:

const arrFoldT = chain => f => init => mmx =>
  chain(mmx) (mx => {
    const go = (acc, i) =>
      i === mx.length
        ? acc
        : chain(mx[i]) (x =>
            go(f(acc) (x), i + 1))
//          ^^^^^^^^^^^^^^^^^^^^^ non-tail call position
    return go(init, 0);
  });

如您所见,该实现不是堆栈安全的。然而,堆栈安全只是另一个可以通过 monad 编码的计算效果。我为Trampoline 类型实现了一个:

const monadRec = o => {
  while (o.tag === "Chain")
    o = o.f(o.x);

  return o.tag === "Of"
    ? o.x
    : _throw(new TypeError("unknown case"));
};

const recChain = mx => fm =>
  mx.tag === "Chain" ? Chain(mx.x) (x => recChain(mx.f(x)) (fm))
    : mx.tag === "Of" ? fm(mx.x)
    : _throw(new TypeError("unknown case"));

const Chain = x => f =>
  ({tag: "Chain", f, x});

const Of = x =>
  ({tag: "Of", x});

虽然实现很简单,但应用程序却并非如此。我很确定我完全错误地应用它:

const mmx = Of(
  Array(1e5)
    .fill(Chain(1) (x => Of(x))));
//                  ^^^^^^^^^^^^ redundant continuation

const main = arrFoldT(recChain)
  (acc => y => recMap(x => x + y) (acc))
    (Of(0))
      (mmx);

monadRec(main); // 100000

创建大型有效数组时,我需要使用Chain,因为Of 发出控制流突破蹦床的信号。另一方面,对于Chain,我必须指定一个多余的延续。

我的第一个想法是翻转Chain 的参数并依赖部分应用,但这不适用于当前的实现。

有没有办法更有效地使用类型?

这是一个工作示例:

// ARRAYT

const arrFoldT = chain => f => init => mmx =>
  chain(mmx) (mx => {
    const go = (acc, i) =>
      i === mx.length
        ? acc
        : chain(mx[i]) (x =>
            go(f(acc) (x), i + 1))

    return go(init, 0);
  });

// TRAMPOLINE

const monadRec = o => {
  while (o.tag === "Chain")
    o = o.f(o.x);

  return o.tag === "Of"
    ? o.x
    : _throw(new TypeError("unknown case"));
};

const Chain = x => f =>
  ({tag: "Chain", f, x});

const Of = x =>
  ({tag: "Of", x});

// Functor

const recMap = f => tx =>
  Of(f(tx.x));

// Monad

const recChain = mx => fm =>
  mx.tag === "Chain" ? Chain(mx.x) (x => recChain(mx.f(x)) (fm))
    : mx.tag === "Of" ? fm(mx.x)
    : _throw(new TypeError("unknown case"));

const recOf = Of;

// MAIN

const mmx = Of(
  Array(1e5)
    .fill(Chain(1) (x => Of(x))));

const main = arrFoldT(recChain)
  (acc => y => recMap(x => x + y) (acc))
    (Of(0))
      (mmx);

console.log(
  monadRec(main)); // 100000

【问题讨论】:

标签: javascript functional-programming monads monad-transformers trampolines


【解决方案1】:

首先,你的数组单子转换器的定义是错误的。

ArrayT m a = m (Array (m a))

上述类型定义未正确交错底层 monad。

以下是上述数据类型的示例值。

of([of(1), of(2), of(3)])

这种数据类型有几个问题。

  1. 对数组末尾没有影响。
  2. 效果没有排序。因此,它们可以按任何顺序执行。
  3. 底层 monad 包装了单个元素以及整个数组。这是错误的。

以下是正确数组单子转换器类型的示例值。

of([1, of([2, of([3, of([])])])])

请注意。

  1. 数组末尾有效果。
  2. 效果已排序。这是因为数据类型是递归定义的。
  3. 底层 monad 包装了数组的各个步骤。它不会再次包装整个数组。

现在,我明白你为什么要定义ArrayT m a = m (Array (m a))。如果m = Identity 则返回一个实际的Array a,它支持元素的随机访问。

of([of(1), of(2), of(3)]) === [1, 2, 3]

另一方面,递归数组monad转换器类型在m = Identity时返回一个链表。

of([1, of([2, of([3, of([])])])]) === [1, [2, [3, []]]]

但是,当底层 monad 为 Identity 时,无法创建一个合法的数组 monad 转换器类型,它也返回一个实际的数组。这是因为 monad 转换器本质上是代数数据结构,而数组不是代数的。

最接近的方法是定义ArrayT m a = Array (m a)。但是,只有当底层 monad 是可交换的时,这才会满足 monad 定律。

请记住,在定义 monad 转换器数据类型时。

  1. 底层 monad 一次最多只能包装一个值。
  2. 必须嵌套底层 monad,才能正确排序和交错效果。

回来,Trampoline monad 就是 Free monad。我们可以这样定义。

// pure : a -> Free a
const pure = value => ({ constructor: pure, value });

// bind : Free a -> (a -> Free b) -> Free b
const bind = monad => arrow => ({ constructor: bind, monad, arrow });

// thunk : (() -> Free a) -> Free a
const thunk = eval => ({ constructor: thunk, eval });

// MonadFree : Monad Free
const MonadFree = { pure, bind };

// evaluate : Free a -> a
const evaluate = expression => {
    let expr = expression;
    let stack = null;

    while (true) {
        switch (expr.constructor) {
            case pure:
                if (stack === null) return expr.value;
                expr = stack.arrow(expr.value);
                stack = stack.stack;
                break;
            case bind:
                stack = { arrow: expr.arrow, stack };
                expr = expr.monad;
                break;
            case thunk:
                expr = expr.eval();
        }
    }
};

我还将从我的previous answer 复制我的数组 monad 转换器的实现。

// Step m a = null | { head : a, tail : ListT m a }
// ListT m a = m (Step m a)

// nil : Monad m -> ListT m a
const nil = M => M.pure(null);

// cons : Monad m -> a -> ListT m a -> ListT m a
const cons = M => head => tail => M.pure({ head, tail });

// foldr : Monad m -> (a -> m b -> m b) -> m b -> ListT m a -> m b
const foldr = M => f => a => m => M.bind(m)(step =>
    step ? f(step.head)(foldr(M)(f)(a)(step.tail)) : a);

因此,当底层 monad 为 Free 时,操作是堆栈安全的。

// replicate :: Number -> a -> ListT Free a
const replicate = n => x => n ?
    cons(MonadFree)(x)(thunk(() => replicate(n - 1)(x))) :
    nil(MonadFree);

// map : (a -> b) -> Free a -> Free b
const map = f => m => bind(m)(x => pure(f(x)));

// add : Number -> Free Number -> Free Number
const add = x => map(y => x + y);

// result : Free Number
const result = foldr(MonadFree)(add)(pure(0))(replicate(1000000)(1));

console.log(evaluate(result)); // 1000000

把它们放在一起。

// pure : a -> Free a
const pure = value => ({ constructor: pure, value });

// bind : Free a -> (a -> Free b) -> Free b
const bind = monad => arrow => ({ constructor: bind, monad, arrow });

// thunk : (() -> Free a) -> Free a
const thunk = eval => ({ constructor: thunk, eval });

// MonadFree : Monad Free
const MonadFree = { pure, bind };

// evaluate : Free a -> a
const evaluate = expression => {
    let expr = expression;
    let stack = null;

    while (true) {
        switch (expr.constructor) {
            case pure:
                if (stack === null) return expr.value;
                expr = stack.arrow(expr.value);
                stack = stack.stack;
                break;
            case bind:
                stack = { arrow: expr.arrow, stack };
                expr = expr.monad;
                break;
            case thunk:
                expr = expr.eval();
        }
    }
};

// Step m a = null | { head : a, tail : ListT m a }
// ListT m a = m (Step m a)

// nil : Monad m -> ListT m a
const nil = M => M.pure(null);

// cons : Monad m -> a -> ListT m a -> ListT m a
const cons = M => head => tail => M.pure({ head, tail });

// foldr : Monad m -> (a -> m b -> m b) -> m b -> ListT m a -> m b
const foldr = M => f => a => m => M.bind(m)(step =>
    step ? f(step.head)(foldr(M)(f)(a)(step.tail)) : a);

// replicate :: Number -> a -> ListT Free a
const replicate = n => x => n ?
    cons(MonadFree)(x)(thunk(() => replicate(n - 1)(x))) :
    nil(MonadFree);

// map : (a -> b) -> Free a -> Free b
const map = f => m => bind(m)(x => pure(f(x)));

// add : Number -> Free Number -> Free Number
const add = x => map(y => x + y);

// result : Free Number
const result = foldr(MonadFree)(add)(pure(0))(replicate(1000000)(1));

console.log(evaluate(result)); // 1000000

【讨论】:

  • 数组不是代数的,因此它们不能转换为合法的转换器,这是有道理的。有趣的是,如果你坚持使用索引,该类型的行为就像一个转换器。我创建了一个 ArrayT/MaybeArrayT/Writer 堆栈,它按预期工作。当然,不能保证这适用于每个 monad。
  • 顺便说一句,您可以将包含在基本效果中的整个数组视为 nil 情况,即基本效果至少发生一次。我知道这只有在杂乱无章的无类型设置中才有可能,但也许如果你用力挤压..
  • “有趣的是,如果你坚持索引,该类型的行为就像一个转换器。”你的意思是固定长度的数组吗?
  • 不,包裹在效果中的整个数组不是基本情况。问题是如果你想访问数组的第一个元素,那么你需要执行两个效果。对整个数组的一种影响和对值的另一种影响。这是不正确的。
  • 我假设一个不可变数组和策略只使用给定的索引遍历它。我猜如果你使用Either 等进行组合,额外的效果并不重要。它可能对编码像 IO/Task 这样的副作用的 monad 有影响,例如,我不知道 Writer 的行为。变压器坏了,可能不值得努力。
猜你喜欢
  • 2014-08-03
  • 2018-12-20
  • 2015-09-28
  • 2021-02-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多