Волшебные числа

В предыдущей статье была фраза о том, что числа тоже являются функциями и что такое 1 = () =› {}. Итак, давайте копнем немного дальше. Если один — функция, то что тогда два? Предположим, что это функция функции, например

one = () => {}
two = () => () => {}
// which means
two_ = () => one

Проще говоря, здесь цифра — это количество толстых стрелок. Так что ноль это ничто. Один — это одна нулевая глубина, два — две глубины и так далее. Если да, давайте определим основные операции над числами. Приращение должно быть оборачивающим числом в функцию, а уменьшение просто вызывает «числовую функцию». Нам также нужно сравнение с 0, но это легко. Ноль — это ничто, а другие числа — это что-то.

const one = () => {};
const inc = n => () => n;
const dec = n => n();
const if_zero = n => !n;

Еще нужны какие-то утилиты только для вывода результата на экран, но для самого расчета они не нужны, так что без комментариев.

const success = (n, f, acc) =>
    n ? success(dec(n), f, f(acc)) : acc;
const toJS = n => success(n, x => x + 1, 0);
const print = n => console.log(toJS(n));

Арифметика

Когда у нас есть числа, декремент и приращение, мы можем определить сложение. Сложить два числа означает увеличить одно из них столько раз, сколько можно уменьшить другое.

// x + y = (x + 1) + (y - 1)
const add = (x, y) => if_zero(y) ? n : add(inc(x), dec(y));

Забыл упомянуть, что здесь мы реализуем систему положительных целых чисел, но это просто для простоты.

Вычитание точно такое же, как сложение, за исключением того, что есть два уменьшения.

const sub = (n, m) => if_zero(m) ? n : sub(dec(n), dec(m));

Умножение и деление немного сложнее, но все же просто, если вы помните, что умножение означает просто прибавление первого множителя к самому себе столько раз, что вы можете уменьшить второй множитель, а деление — это просто подсчет того, сколько раз вы можете вычесть делитель из делимого.

const mul = (n, m, acc=0) =>
    if_zero(n)
        ? acc
        : mul(dec(n), m, add(acc, m));
const div = (n, m, acc=0) =>
    if_zero(n)
        ? acc
        : div(sub(n, m), m, inc(acc));

Создать функцию без аккумулятора вполне возможно, но для примера это не важно. Это здесь просто для краткости.

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

const two    = add(one, one);    // 1 + 1 = 2
const three  = add(one, two);    // 1 + 2 = 3
const four   = add(three, one);  // 3 + 1 = 4
const two2   = sub(three, one);  // 3 - 1 = 2
const six    = mul(two, three);  // 2 * 3 = 6
const three2 = div(six, two);    // 6 / 2 = 3

А как насчет еще более сложных операций, таких как экспоненты, корни и т.д. Они не такие сложные, вам просто нужно использовать уже созданный базовый и еще больше расширить его.

Сравнение

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

const compare = (LEFT, RIGHT) => {
   if(if_zero(LEFT) && if_zero(RIGHT)){
       return 'equal';
   } else if (if_zero(LEFT)){
       return 'smaller';
    } else if (if_zero(RIGHT)){
       return 'bigger';
    } else {
      return equal(dec(LEFT), dec(RIGHT));
   }
}

Итак, в этой части мы доказали, что можно построить систему счисления, основанную исключительно на функциях. Но это был довольно наивный подход, и чтобы сделать эту систему полной, нам нужно начать с чего-то другого. Числа на самом деле не являются основными. В компьютерном мире каждое число представлено в виде цепочки битов. На самом деле это цепочка логических значений. И если вы думаете об этом, вся информация в компьютерном мире хранится в цепочке логических значений. Итак, давайте начнем с нуля с самого BOOlean.

логический

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

Сначала об утверждении «если». Он должен состоять из трех частей: «предикат», «затем» закрытие и «иначе» закрытие. Все они являются функциями, и если это так, мы могли бы определить условие как:

const IF = (predicate, left, right) => predicate(left, right);

Предикат должен быть вызван и решить, что вызывать дальше. А если так, то предикаты «истина» и «ложь» просты. Один из них всегда возвращает замыкание «тогда», а другой всегда возвращает «иначе».

const TRUE = (left, right) => left;
const FALSE = (left, right) => right;
IF(TRUE, 'true', 'false')  // returns true
IF(FALSE, 'true', 'false') // false

Можем ли мы определить логические операции над нашими «истинными» и «ложными»? Конечно. Например «и».

const AND = (left, right) =>
    IF(left, IF(right, TRUE, FALSE), FALSE);

Единственное, что лучше изменить прямо сейчас, это начать использовать «каррирование». Это сделало бы правила более краткими и их было бы легче комбинировать. Если вы не знаете, что это значит, это просто. Это просто заставить функцию принимать все аргументы один за другим, вот так.

const add = (x, y, z) => x + y + z;
const curried_add = x => y => z => x + y + z;

Теперь вернемся к булевым значениям. Добавим немного карри и еще пару правил.

const IF = predicate => THEN => ELSE => predicate(THEN)(ELSE);
const TRUE = THEN => ELSE => THEN;
const FALSE = THEN => ELSE => ELSE;
const AND = left => right => left(right)(FALSE);
const OR = left => right => left(TRUE)(right);
const NOT = predicate => THEN => ELSE => predicate(ELSE)(THEN);
console.log(
    IF(NOT(FALSE))('true')('false') // true
);

Понимаете? Правила стали намного чище.

Номера два

Теперь мы готовы реализовать числа. Обычно «ноль» равен «false», начнем с этого факта.

const ZERO = _ => x => x;
// Actually could be written as 'const ZERO = FALSE;'

Как насчет набора текста? Здесь как-то иначе, потому что все основано на единственном типе — функции. И имена переменных тоже не так важны, поскольку функция может использоваться всякий раз, когда передается структура функции. Такие функции иногда называют комбинаторами. Так что дело только в порядке звонков. И помните, все функции теперь каррированы.

Такие функции иногда называют комбинаторами. Например:
х => х — это комбинатор. x =› x(x) — это M комбинатор, и их гораздо больше.

Как будут выглядеть другие числа в этой системе? Помните, что мы полагаемся на то, насколько глубоко спрятана основная ценность. А здесь «ноль» означает «функция «f» была вызвана в ноль раз на «x»». Таким образом, «один» автоматически означает, что «f» нужно вызвать один раз, два — дважды и так далее.

const ZERO = fn => x => x;
const ONE  = fn => x => fn(x);
const TWO  = fn => x => fn(fn(x));

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

const SUCCESSOR = num => fn => x => fn(num(fn)(x));

Теперь мы могли легко построить числа, заменив предыдущий.

const one = SUCCESSOR(ZERO);
const two = SUCCESSOR(one);
const three = SUCCESSOR(two);

Чтобы проверить, как это работает, нам нужно преобразовать наши числа в представление JavaScript. Для этого просто вызовите «+1» число раз на «0»

const TO_JS = n => n(x => x + 1)(0);

Для сложения нам не понадобится сравнение с нулем, но все же определим его.

const IS_ZERO = num => num(_ => FALSE)(TRUE);

Если это ноль, то возвращается начальное значение «ИСТИНА», в противном случае возвращается значение переданной функции, которая всегда возвращает «ЛОЖЬ».

Теперь вернемся к математике. Дополнение простое.

N + M = N-кратное увеличение M

который:

const ADD = n => m => n(SUCCESSOR)(m);

«n» раз применяется преемник, наша функция приращения, к «m». Что дает нам возможность суммировать числа.

Вы заметили, насколько это стало просто по сравнению с наивным подходом, который мы использовали раньше?

const one = SUCCESSOR(ZERO);
const two = SUCCESSOR(one);
const three = ADD(one)(two);
console.log('--->', TO_JS(three)); // 3

Что уж говорить о других операциях. Начнем с умножения и показателя степени. Теперь они удивительно элегантны. Умножение — это «сочинение», просто перенос одного числа в другое.

const compose = f => g => x => f(g(x));
const MULT = compose;

А pow еще проще.

const POW = n => m => n(m);

Как насчет вычитания, деления и сравнения чисел. Они же простые.

"x" меньше или равно "y", когда "x - y ≤ 0".
"x" равно "y", когда "x ≤ y и y ≤ x".
Вычитание то же, что и сложение, только вместо приращения используйте декремент.

const LE = x => y => IS_ZERO(SUB(x)(y))
const EQ = x => y => AND(LE(x)(y))(LE(y)(x))
const SUB = x => y => y(PREDECESSOR)(x)

Единственная маленькая оговорка, что у нас пока нет того ПРЕДШЕСТВА. Это не полная наука о ракетах, нам просто нужно смотреть на число как на минус, парная структура, о которой мы говорили в предыдущей статье, но сейчас это определенно слишком.

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

И, кстати, настоящее имя мастера Фу было Алонзо. А на самом деле их было больше, просто погуглите «лямбда-исчисление» и просветитесь ;)