将专门针对列表的未来主义表达式转化为命令式循环。

4

我一直在尝试翻译这个递归的Haskell实现,它是专门针对List的futumorphism。

futuL :: (a -> Maybe (b, ([b], Maybe a))) -> a -> [b]
futuL f x = case f x of
  Nothing -> []
  Just (y, (ys, mz)) -> y : (ys ++ fz)
    where fz = case mz of
      Nothing -> []
      Just z -> futuL f z

将其转换为JavaScript命令式循环非常困难(至少对于我来说)。
const None = ({type: "Option", tag: "None"});
const Some = x => ({type: "Option", tag: "Some", runOption: x});

const arrFutu = coalg => x => { // futuL f x
  const acc = []; // ~ fz

  while (true) {
    let optX = coalg(x); // f x

    switch (optX.tag) { // case
      case "None": return acc; // Nothing -> []

      case "Some": {
        let [y, [ys, optX_]] = optX.runOption; // Just (y, (ys, mz))

        switch(optX_.tag) {
          case "None": {
            arrPushFlat(acc) ((ys.unshift(y), ys)); // y : (ys ++ [])
            return acc;
          }

          case "Some": { // y : (ys ++ futuL f z)
            arrPushFlat(acc) ((ys.unshift(y), ys)); 
            x = optX_.runOption;
            break;
          }

          default: throw new UnionError("invalid tag");
        }

        break;
      }

      default: throw new UnionError("invalid tag");
    }
  }
};

我很难在心里解析Haskell代码,特别是包含递归调用的where部分。我猜想这个调用不是尾部位置,因此我需要为我的JS循环使用显式堆栈(acc)。

我的翻译正确吗?

1个回答

2
由于Haskell是惰性的,因此可以在其余部分计算完成之前开始消耗“futu”返回的列表开头。用JavaScript术语来说,这最好用生成器来建模。
例如(为简单起见,我使用了null值来表示None):
const arrFutu = coalg => function*(seed) {
    while (true) {
        const next = coalg(seed);
        if (next) {
            // Maybe (b, ([b], Maybe a)), using null for None & flattening nested tuple   
            const [item, items, nextseed] = next; 
            yield item;
            yield* items;
            // Maybe a, using null for None 
            if (nextseed) { 
                seed = nextseed;
                continue; // Continue iterating if the seed isn't null.
            }
        } 
        return;
    }
}

使用一个类似以下的余代数示例:

const coalg1 = seed => {
    if (seed < 5) {
        return ['a', ['a','a'], seed + 1];
    } else if (seed < 10) {
        return ['b', ['b','b'], seed + 1];
    } else if (seed == 10) {
        return ['c', ['c'], null];
    }
    return null;
}

for (const x of arrFutu(coalg1)(0)) {
    console.log(x);
}

for (const x of arrFutu(coalg1)(20)) {
    console.log(x);
}

将futu改为递归而不是迭代会更好,但似乎JavaScript尾调用优化在生成器中无法工作


顺便说一下,即使Haskell代码不是尾递归的,递归也是"受保护的":递归调用发生在一个或多个数据构造函数(这里是列表构造函数)内部,当客户端消耗返回的列表时,调用可以被延迟直到构造函数被“剥离”。


感谢您添加示例,非常好的答案。我想懒惰求值也很重要,以理解futu和histo的对偶。 - user5536315
1
不幸的是,即使它是规范的一部分(没有主要的浏览器供应商实现它或者不会很快实现它),JavaScript 中没有尾递归优化。是的,我知道 guarded recursion/TCO modulo cons 并且已经在 JS 中以 trampoline 的形式实现了它。 - user5536315
2
@bob 惰性求值更适合 futu,因为 futu 是一个展开,一种从种子构建值的"anamorphism"。构建的值甚至可能是无限的。同时,histos 是"catamorphims",可以拆解已存在的有限值。在那里,严格求值效果很好。 histos 相对于普通 catamorphisms 的特殊之处在于,在每个步骤中,它们都可以访问所有先前子结果记录。 - danidiaz
那听起来很合理。我只是发现对于List类型,histo似乎是基于cata实现的,在这种情况下它只是右折叠foldr,即惰性求值的右关联折叠。我需要在这个主题上做一些研究。对我个人而言,惰性求值是Haskell中最复杂的方面之一。 - user5536315

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接