【问题标题】:How to fold data structures from non-tail recursive algorithms?如何从非尾递归算法折叠数据结构?
【发布时间】:2019-10-18 09:58:14
【问题描述】:

我有一个可变参数提升函数,它允许扁平的单子链没有深度嵌套的函数组合:

const varArgs = f => {
  const go = args =>
    Object.defineProperties(
      arg => go(args.concat(arg)), {
        "runVarArgs": {get: function() {return f(args)}, enumerable: true},
        [TYPE]: {value: "VarArgs", enumerable: true}
      });

  return go([]);
};

const varLiftM = (chain, of) => f => { // TODO: replace recursion with a fold
  const go = (ms, g, i) =>
    i === ms.length
      ? of(g)
      : chain(ms[i]) (x => go(ms, g(x), i + 1));

  return varArgs(ms => go(ms, f, 0));
};

它有效,但我想通过折叠从递归中抽象出来。正常折叠似乎不起作用,至少不能与 Task 类型一起使用,

const varLiftM = (chain, of) => f =>
  varArgs(ms => of(arrFold(g => mx => chain(mx) (g)) (f) (ms))); // A

因为A 行中的代数会为每次迭代返回一个Task,而不是一个部分应用的函数。

如何用折叠替换非尾递归?

这是当前递归实现的一个工作示例:

const TYPE = Symbol.toStringTag;

const struct = type => cons => {
  const f = x => ({
    ["run" + type]: x,
    [TYPE]: type,
  });

  return cons(f);
};

// variadic argument transformer

const varArgs = f => {
  const go = args =>
    Object.defineProperties(
      arg => go(args.concat(arg)), {
        "runVarArgs": {get: function() {return f(args)}, enumerable: true},
        [TYPE]: {value: "VarArgs", enumerable: true}
      });

  return go([]);
};

// variadic monadic lifting function

const varLiftM = (chain, of) => f => { // TODO: replace recursion with a fold
  const go = (ms, g, i) =>
    i === ms.length
      ? of(g)
      : chain(ms[i]) (x => go(ms, g(x), i + 1));

  return varArgs(ms => go(ms, f, 0));
};

// asynchronous Task

const Task = struct("Task") (Task => k => Task((res, rej) => k(res, rej)));

const tOf = x => Task((res, rej) => res(x));

const tMap = f => tx =>
  Task((res, rej) => tx.runTask(x => res(f(x)), rej));

const tChain = mx => fm =>
  Task((res, rej) => mx.runTask(x => fm(x).runTask(res, rej), rej));

// mock function

const delay = (ms, x) =>
  Task(r => setTimeout(r, ms, x));

// test data

const tw = delay(100, 1),
  tx = delay(200, 2),
  ty = delay(300, 3),
  tz = delay(400, 4);

// specialization through partial application

const varAsyncSum =
  varLiftM(tChain, tOf) (w => x => y => z => w + x + y + z);

// MAIN

varAsyncSum(tw) (tx) (ty) (tz)
  .runVarArgs
  .runTask(console.log, console.error);

console.log("1 sec later...");

[编辑] 根据 cmets 的需要,我的折叠实现:

const arrFold = alg => zero => xs => {
  let acc = zero;

  for (let i = 0; i < xs.length; i++)
    acc = alg(acc) (xs[i], i);

  return acc;
};

【问题讨论】:

  • 这是很多缩小的代码。你能简化这个例子吗?
  • @cristy 那不是缩小的,那是“函数式编程”:)
  • 好吧,将你的参数命名为msgxffmoftxres、@9876543333然后对我来说使用所有这些(几乎一次)看起来像是不必要的缩小,除非你知道它是关于什么的,否则会使代码更难阅读。
  • @Cristy 大多数(如果不是全部的话)在函数式编程中相当地道。类似于您如何命名变量i,如果它是某种计数器或索引。 f - 函数,g 也是一个函数(按字母顺序排列在 f 之后,类似于 j 在声明 i 之后用作计数器),fm - 返回 monad 的函数,ms - monads 复数,所以是一个数组,of 函数将类型提升到 monad。 x - 输入变量的通用名称。 resrej - 结果(成功)和拒绝(失败)。 txtytz - 任务1、任务2、任务3。
  • 短名称只是反映了代码的通用性。例如,您可以将 varLiftM 与任何 monad 一起使用。通过@VLAZ 完成名称图例:mx monadic value、ms monadic values、fm monadic action(返回 monad 的函数)。 tx 仅表示包装在类型中的值,而没有说明约束(即,如果它是一元的、应用的、函数的、遍历的、可折叠的等)。

标签: javascript recursion functional-programming fold recursion-schemes


【解决方案1】:

of 打电话给arrFold 似乎有点不合适。

我不确定您的 arrFold 是右折叠还是左折叠,但假设它是右折叠,您将需要使用带有闭包的连续传递样式,就像您在递归实现中所做的那样:

varArgs(ms => of(arrFold(g => mx => chain(mx) (g)) (f) (ms)))

变成

varArgs(ms => arrFold(go => mx => g => chain(mx) (x => go(g(x)))) (of) (ms) (f))

左折叠,你可以写

varArgs(arrFold(mg => mx => chain(g => map(g) (mx)) (mg)) (of(f)))

但您需要注意,这会构建与正确折叠不同的调用树:

of(f)
chain(of(f))(g0 => map(m0)(g0))
chain(chain(of(f))(g0 => map(m0)(g0)))(g1 => map(m1)(g1))
chain(chain(chain(of(f))(g0 => map(m0)(g0)))(g1 => map(m1)(g1)))(g2 => map(m2)(g2))

vs(已经应用了延续)

of(f)
chain(m0)(x0 => of(f(x0)))
chain(m0)(x0 => chain(m1)(x1 => of(f(x0)(x1))))
chain(m0)(x0 => chain(m1)(x1 => chain(m2)(x2) => of(f(x0)(x1)(x2)))))

根据单子定律,它们的计算结果应该相同,但在实践中,一个可能比另一个更有效。

【讨论】:

  • 聪明——至少我认为是这样。明天我需要深入研究一下。 +1 因为我很确定我明天会学到一些东西。
  • arrFold 函数是左折叠。
  • @Bergi 构建不同的调用树 - 好的,这仅在二元运算符不是关联/可交换的情况下才重要。此外,至少对于Arrays,您可以通过分别将flip/Array.prototype.reverse 应用于运算符和使用的数组来将任何左折叠转换为右折叠,反之亦然。这可能会导致性能问题...
【解决方案2】:

对于这个特定的用例,您不需要 monad 的全部功能。您只需要应用函子:

// type Cont r a = (a -> r) -> r

// type Async a = Cont (IO ()) a

// pure :: a -> Async a
const pure = a => k => k(a);

// ap :: Async (a -> b) -> Async a -> Async b
const ap = asyncF => asyncA => k => asyncF(f => asyncA(a => k(f(a))));

// delay :: (Number, a) -> Async a
const delay = (ms, a) => k => setTimeout(k, ms, a);

// async1, async2, async3, async4 :: Async Number
const async1 = delay(100, 1);
const async2 = delay(200, 2);
const async3 = delay(300, 3);
const async4 = delay(400, 4);

// sum :: Number -> Number -> Number -> Number -> Number
const sum = a => b => c => d => a + b + c + d;

// uncurry :: (a -> b -> c) -> (a, b) -> c
const uncurry = f => (a, b) => f(a)(b);

// result :: Async Number
const result = [async1, async2, async3, async4].reduce(uncurry(ap), pure(sum));

// main :: IO ()
result(console.log);
console.log("1 second later...");

如果需要,您可以定义一个应用上下文函数(即apply),如下所示:

const apply = (asyncF, ...asyncArgs) => asyncArgs.reduce(uncurry(ap), asyncF);

const result = apply(pure(sum), async1, async2, async3, async4);

如果你 curry 这个函数,那么你可以创建一个lift 函数:

const apply = asyncF => (...asyncArgs) => asyncArgs.reduce(uncurry(ap), asyncF);

const lift = f => apply(pure(f));

const asyncSum = lift(sum);

const result = asyncSum(async1, async2, async3, async4);

注意reduce 等同于arrFold。因此,lift 等价于 varLiftM

【讨论】:

  • 我想我得到了提示:不要太复杂,也许我的varArgs 方法太复杂了。我会记住这一点。哦,我知道 applicative(取决于前一个效果)和 monad(可能还取决于前一个效果的值)之间的区别。在工作中,我会尽可能多地使用前者。
  • 是的,确实很复杂。问题在于,尽管 JavaScript 不是一种好的函数式编程语言,但人们尝试使用它编写复杂的函数式程序。 JavaScript 比 Haskelline 代码更适合 Pythonic 代码。然而,因为它试图借用无数语言的元素,它最终既不像 Python 那样通用,也不像 Haskell 那样优雅。如果您真的渴望成为一名更好的函数式程序员,请尝试使用 JavaScript 为函数式编程语言编写编译器。 en.m.wikibooks.org/wiki/Write_Yourself_a_Scheme_in_48_Hours
  • 如果您渴望成为一名出色的程序员,那么编译器是一个很好的项目。您不仅将学习编程语言的设计方式,还将体验如何编写和维护一个重要的软件。争取的最佳目标是构建一个自托管编译器(即,一个用相同源语言编写的源语言编译器,它能够将自己编译成某种目标语言)。我喜欢编译器的构造。在我决定加入这个行业之前,我正在攻读博士学位。如果您对此提出问题,我将非常乐意提供帮助
  • 感谢您的建议。也许你还记得我一年前的运行时类型验证器项目。我一直想迈出下一步,但到目前为止我还没有找到时间。
  • 运行时类型验证很好,但它永远不会像编译时类型检查/推断那样强大。你只能在运行时做这么多。
猜你喜欢
  • 2020-07-20
  • 1970-01-01
  • 2013-08-06
  • 2015-02-10
  • 2018-03-17
  • 1970-01-01
  • 2016-04-07
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多