Выразите футуморфизм, специализированный для списков, в виде императивного цикла

Я пытался перевести эту рекурсивную реализацию Haskell футуморфизма, специализированного на Lists

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, содержащую рекурсивный вызов. Я предполагаю, что этот вызов не находится в хвостовой позиции, поэтому мне нужен явный стек (acc) для моего цикла JS.

Верен ли мой перевод?


person Iven Marquardt    schedule 01.06.2019    source источник


Ответы (1)


Поскольку 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);
}

Было бы неплохо сделать футу рекурсивным, а не итеративным, но кажется, что хвост JavaScript оптимизация вызовов не работает с генераторами.


Между прочим, даже если код Haskell не является хвостовой рекурсией, рекурсия "guarded": рекурсивный вызов происходит внутри одного или нескольких конструкторов данных (здесь конструкторы списков), и вызов может быть отложен до тех пор, пока конструкторы не будут "отделены" клиентом, когда он потребляет возвращенный список.

person danidiaz    schedule 01.06.2019
comment
Спасибо за добавление примера - отличный ответ. Я предполагаю, что ленивая оценка также важна для понимания двойного футу, гисто. - person Iven Marquardt; 01.06.2019
comment
К сожалению, в Javascript нет TCO, несмотря на то, что он является частью спецификации (ни один крупный поставщик браузеров не реализовал его и не будет реализовывать в ближайшее время. И да, я знаю минусы защищенной рекурсии/TCO по модулю, а также реализовал его в виде батут на JS. - person Iven Marquardt; 01.06.2019
comment
@bob Ленивая оценка лучше подходит для футу, потому что футу — это развертывание, анаморфизм, который строит значение из семени. Значение, которое создается, может быть даже бесконечным. Между тем гистос — это катаморфимы, разрушающие существующую конечную ценность. Здесь хорошо работает строгая оценка. Особенность гист по отношению к обычным катаморфизмам заключается в том, что на каждом этапе они имеют доступ к записи всех предыдущих промежуточных результатов. - person danidiaz; 01.06.2019
comment
Звучит разумно. Я просто подумал, что для типа List тип histo кажется реализованным с точки зрения cata, который в данном контексте является просто foldr, то есть лениво вычисляемой правой ассоциативной складкой. Мне нужно провести небольшое исследование по этому вопросу. Лично для меня ленивые вычисления — один из самых сложных аспектов Haskell. - person Iven Marquardt; 01.06.2019