Что было бы хорошим примером endofunctor, который не является тождественным функтором?

В  Профессор Frisby представляет Composable Functional JavaScript был введен функтор идентичности:

const Box = x => 
   ({ 
       map:  f => Box(f(x)),
       fold: f => f(x)           // for testing
   })

Я потратил большую часть функций дня на понимание и почему приведенный выше код JavaScript на самом деле является функтором идентификации. Поэтому я решил изменить его, чтобы получить "реальный" функтор, который не является функтором идентичности. Я придумал это:

const Endo = x =>
   ({ 
       map:  f => Endo(f(x).split('')),
       fold: f => f(x).split('') // for testing
   })

Мое рассуждение состоит в том, что с Box, Id_Box: Box -> Box и Id_Box f = f. Endo также будет отображаться для себя, но Endo(f): Endo(x) -> Endo(y) (if f: x -> y).

Я на правильном пути?

EDIT: Заменили string на более общий x, как это было в исходных примерах.

Ответ 1

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

Что касается функтора, то вкратце это

  • структура данных (поле в вашем примере)
  • который может поддерживать операцию сопоставления (подумайте Array.prototype.map)
  • и что операция отображения соответствует тождеству: xs === xs.map(x => x)
  • ... и состав: xs.map(f).map(g) === xs.map(f . g), где . - составная функция.

Что это. Не больше, не меньше. Глядя на ваш Box, это структура данных, которая имеет функцию map (проверьте 1 и 2) и что функция карты выглядит так, как будто она должна уважать личность и состав (проверьте 3 и 4). Так что это функтор. Но это ничего не делает, поэтому он является тождественным функтором. Функция fold не является строго необходимой, она просто предоставляет способ "развернуть" поле.

Для полезного функтора рассмотрим массивы JavaScript. Массивы действительно что-то делают: именно они содержат несколько значений, а не только одну. Если массив может иметь только один элемент, это будет ваш Box. Для наших целей мы будем притворяться, что они могут просто придерживаться значений одного и того же типа для простых вещей. Таким образом, массив представляет собой структуру данных, которая имеет функцию отображения, которая учитывает идентичность и состав.

let plus = x => y => x + y;
let mult = x => y => x * y;
let plus2 = plus(2);
let times3 = mult(3);
let id = x => x;
let compose = (...fs) => arg => fs.reverse().reduce((x, f) => { return f(x) }, arg);  

// Here we need to stringify the arrays as JS will compare on 
// ref rather than value. I'm omitting it after the first for
// brevity, but know that it necessary.
[1,2,3].map(plus2).toString() === [3,4,5].toString(); // true
[1,2,3].map(id) === [1,2,3]; // true
[1,2,3].map(plus2).map(times3) === [1,2,3].map(compose(times3, plus2)); // true

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

Итак, теперь рассмотрим еще одну повсеместную структуру данных JavaScript - объект. Нет встроенной функции map для объектов. Можем ли мы сделать их функторами? Предположим еще, что объект является однородным (имеет только ключи к одному типу значения, в этом примере Number):

let mapOverObj = obj => f => {
  return Object.entries(obj).reduce((newObj, [key, value]) => {
    newObj[key] = f(value);
    return newObj;
  }, {});
};

let foo = { 'bar': 2 };
let fooPrime = mapOverObj(foo)(plus2); // { 'bar': 4 }

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