Вычисление факториала на числах Чёрча

в 10:39, , рубрики: javascript, КодоБред, ненормальное программирование, функциональное программирование, числа чёрча

Доброго дня, друзья!

Тема функционального программирования раскрыта на Хабре весьма неплохо, есть целая куча статей посвященных λ-исчислению, числам Чёрча и подобным темам, так что ничего нового я не открою, зато мы напишем одну бесполезную и крайне неэффективную программу.

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

Начнем с определения факториала, и посмотрим, что нам понадобится в процессе решения задачи:

var fact = function (n) {
  if (n === 0) return 1;
  return n * fact(n - 1);
};

Итак, нам потребуются функции, логические значения (для результата операции сравнения с нулем), условный оператор, операции умножения и вычитания единицы, кроме того нужно будет реализовать рекурсивный вызов функции.

Готовы?

Начнем с простого, с логических значений True, False и функции If. True и False представлены каррированными функциями двух аргументов осуществляющими выбор первого либо второго аргумента. True выбирает первый аргумент, а False — второй. If принимает три аргумента, логическое значение, ветку then и ветку else.

var True = function (x) { 
  return function (y) {
    return x;
  }; 
};

var False = function (x) { 
  return function (y) {
    return y;
  }; 
};

var If = function (p) { 
  return function (t) {
    return function (e) {
      return p(t)(e);
    }
  };
};

Можно побаловаться в консоли выполняя код типа:

console.log(If(True)('foo')('bar'));
console.log(If(False)('foo')('bar'));

Дальше нам потребуются числа. Множество натуральных чисел можно получить определив значение числа НОЛЬ и операцию ПЛЮС_ОДИН. Числа Чёрча, грубо говоря, представляют собой функции двух аргументов, применяющие первый аргумент ко второму n раз, где n — это соответствующее натуральное число.

var Zero = function (f) {
  return function (x) {
    return x;
  };
};

var Succ = function (n) {
  return function (f) {
    return function (x) {
      return f(n(f)(x));
    };
  };
};

Дополнительно определим предикат проверяющий, является ли число нулем, реализация факториала потребует такую проверку. Предикат возвращает False если первый аргумент числа выполнился хотя бы раз.

var IsZero = function (n) {
  return n(function (x) { 
    return False;
  })(True);
};

Проверьте как работают числа:

Succ(Succ(Succ(Zero)))(function (x) { return x + 1; })(0);
console.log(If(IsZero(Zero))('zero')('not zero'));
console.log(If(IsZero(Succ(Zero)))('zero')('not zero'));

Как видите, к нулю трижды прибавилась единица, а предикат корректно распознает переданное число.

Теперь, когда у нас есть все натуральные числа определим функцию умножающую два числа, результат умножения — это функция, которая n раз по m раз применяет свой аргумент.

var Mul = function (n) {
  return function (m) {
    return function (f) {
      return n(m(f));
    };
  };
};

Осталось научиться отнимать единицу от числа. Тут все немного сложнее. Идея вычитания состоит в том, что строится пара чисел (n, n — 1) и берется второй элемент пары. Таким образом нам нужно получить конструктор пары, а так же функции получения первого и второго элемента. После чего мы сможем определить функцию получения предыдущего числа.

var Pair = function (a) {
  return function (b) {
    return function (t) {
      return t(a)(b);
    };
  };
};

var Fst = function (p) {
  return p(True);
};

var Snd = function (p) {
  return p(False);
};

var Pred = function (n) {
  return function (s) {
    return function (z) {
      return Snd(n(function (p) {
        return Pair(s(Fst(p)))(Fst(p)); 
      })(Pair(z)(z)));
    };
  };
};

Поиграемся с парами и вычитанием:

Fst(Pair('foo')('bar')); // => "foo"
Snd(Pair('foo')('bar')); // => "bar"
Pred(Succ(Succ(Zero)))(function (x) { return x + 1; })(0); // => 1

Казалось бы, все уже готово и можно реализовать факториал:

var fact = function (n) {
  return If(IsZero(n))(Succ(Zero))(Mul(n)(fact(Pred(n))));
};

Но есть две проблемы, первая заключается в том, что JavaScript — язык с аппликативным порядком вычислений и наш факториал просто повиснет, т.к. будет выполнять рекурсивный вызов вне зависимости от значения аргумента. Эту проблему легко решить обернув рекурсивный вызов в анонимную функцию и тем самым отложив выполнение.

var fact = function (n) {
  return If(IsZero(n))(Succ(Zero))(function (x) {
    return Mul(n)(fact(Pred(n)))(x);
  });
};

Теперь функция работает корректно:

fact(Succ(Succ(Succ(Zero))))(function (x) { return x + 1; })(0); // => 6

Осталась последняя проблема. Вначале я обещал использовать только анонимные функции, а тут мы видим рекурсивный вызов по имени fact. Нужно от него избавиться и в этом нам поможет Y-комбинатор. Объяснять принцип его работы я не буду, на эту тему есть статьи и на Хабре и вне пределов Хабра. Рекомендую например вот эту блогозапись. Y-комбинатор в аппликативном языке выглядит так:

var Y = function (f) {
  return function (x) {
    return x(x);
  }(function (x) {
    return function (y) {
      return f(x(x))(y);
    };
  });
};

Проверить корректность его работы можно так:

Y(function (f) {
  return function (n) {
    return If(IsZero(n))(Succ(Zero))(function (x) {
      return Mul(n)(f(Pred(n)))(x);
    });
  };
})(Succ(Succ(Succ(Zero))))(function (x) { return x + 1; })(0); // => 6

Теперь подставим факториал в Y-комбинатор и получим конечный вариант:

var fact = function (x) {
  return x(x);
}(function (x) {
  return function (y) {
    return function (f) {
      return function (n) {
        return If(IsZero(n))(Succ(Zero))(function (x) {
          return Mul(n)(f(Pred(n)))(x);
        });
      };
    }(x(x))(y);
  };
});

Для большего удобства определим функции NatToChurch и ChurchToNat:

var NatToChurch = function (n) {
  return n == 0 ? Zero : function (f) {
    return function (x) {
      return f(NatToChurch(n - ChurchToNat(Succ(Zero)))(f)(x));
    };
  };
};

var ChurchToNat = function (n) {
  return n(function (x) {
    return x + 1;
  })(0);
};

Теперь можно ставить эксперименты в консоли:

ChurchToNat(fact(NatToChurch(5))); // => 120

Последний шаг, сделать все подстановки и получить финальную функцию:

Осторожно, очень много функций

var fact = function (x) {
  return x(x);
}(function (x) {
  return function (y) {
    return function (f) {
      return function (n) {
        return function (p) { 
          return function (t) {
            return function (e) {
              return p(t)(e);
            }
          };
        }(function (n) {
          return n(function (x) { 
            return function (x) { 
              return function (y) {
                return y;
              }; 
            };
          })(function (x) { 
            return function (y) {
              return x;
            }; 
          });
        }(n))(function (n) {
          return function (f) {
            return function (x) {
              return f(n(f)(x));
            };
          };
        }(function (f) {
          return function (x) {
            return x;
          };
        }))(function (x) {
          return function (n) {
            return function (m) {
              return function (f) {
                return n(m(f));
              };
            };
          }(n)(f(function (n) {
            return function (s) {
              return function (z) {
                return function (p) {
                  return p(function (x) { 
                    return function (y) {
                      return y;
                    }; 
                  });
                }(n(function (p) {
                  return function (a) {
                    return function (b) {
                      return function (t) {
                        return t(a)(b);
                      };
                    };
                  }(s(function (p) {
                    return p(function (x) { 
                      return function (y) {
                        return x;
                      }; 
                    });
                  }(p)))(function (p) {
                    return p(function (x) { 
                      return function (y) {
                        return x;
                      }; 
                    });
                  }(p)); 
                })(function (a) {
                  return function (b) {
                    return function (t) {
                      return t(a)(b);
                    };
                  };
                }(z)(z)));
              };
            };
          }(n)))(x);
        });
      };
    }(x(x))(y);
  };
});

Осталось ответить на вопрос «Зачем так делать?». Признаюсь честно, на этот вопрос у меня ответа нет. Всем добра!

Автор: f0rk

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js