Сыграем в игру?

Не могли бы вы сказать, не выполняя его, что делает следующий фрагмент?

const ಠ_ಠ = n => (t => t(7)('🐱')(t(5)('🐶')(t(3)('🐭')(x => x)))(n.toString()))(d => s => x => n % d ? x : _ => s + x(''));

(Да, это действительный JavaScript, но суть не в этом…)

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

Итак, убрав титры, давайте посмотрим, что скрывается за хаосом. Кто знает, может быть, это даже интересно.

Сделать это красивее

Первое, что мы должны сделать, это добавить немного форматирования, чтобы мы могли хотя бы понять самый внешний уровень скобок:

const ಠ_ಠ = n =>
    (t => t(7)('🐱')(t(5)('🐶')(t(3)('🐭')(x => x)))(n.toString()))
    (d => s => x => n % d ? x : _ => s + x(''));

Что ж, это помогло

Я имею в виду. Так и было. Вроде.

Давайте посмотрим на нашу функцию, медленно:

  • Он принимает один аргумент, n.
  • Объявляет стрелочную функцию, которая принимает один аргумент, t.
  • И немедленно применяет эту функцию с другой стрелочной функцией в качестве аргумента.

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

const ಠ_ಠ = n => {
    const t = d => s => x => n % d ? x : _ => s + x('');
    
    return t(7)('🐱')(t(5)('🐶')(t(3)('🐭')(x => x)))(n.toString());
}

Видеть? Теперь это начинает выглядеть как что-то, что человек когда-то мог напечатать, может быть, с какими-то человеческими пальцами или чем-то еще, что делают люди.

Нужно больше идентификаторов

Следующее, что мы могли бы сделать, это попытаться добавить в смесь еще несколько идентификаторов.

Кажется, существует шаблон t(number)(string) функций, связанных вместе, поэтому давайте назовем их соответственно. И мы можем поступить с x => x таким же образом.

const ಠ_ಠ = n => {
    const t = d => s => x => n % d ? x : _ => s + x('');
    const cat = t(7)('🐱');
    const dog = t(5)('🐶');
    const mouse = t(3)('🐭');
    const id = x => x;
    return cat(dog(mouse(id)))(n.toString());
}

Хорошо, кажется, мы почти у цели. Все, что нам нужно сделать, это выяснить, что делает t. Это всего лишь тернарный оператор, насколько это сложно?

Часть, где вы смотрите на это, пока это не обретет смысл

Пожалуйста.

Давайте попробуем рассмотреть t поближе:

  • Он принимает три аргумента: d, s и x.
  • Он проверяет, является ли n кратным d.
  • Он возвращает либо x, либо функцию, которая игнорирует свой аргумент и возвращает s, соединенное с результатом вызова x с пустой строкой.

С учетом этих наблюдений, а также аргументов, уже представленных во фрагменте, мы можем сделать некоторые выводы:

  • n и d – это числа.
  • s – это строка.
  • x — это функция из строки в строку.

Убежать за этим

На данный момент, я думаю, мы должны взглянуть на первое полное приложение t, а именно mouse(id):

  • Если n не кратно 3, возвращается id.
  • Если n кратно 3, возвращается функция, которая добавляет 🐭 к результату вызова id с пустой строкой.

Хорошо, это было не так уж плохо? Можем ли мы продолжить? Что произойдет дальше?

После mouse вызывается dog с любой из двух функций, возвращаемых mouse в качестве аргумента:

  • Если n не кратно 5, возвращается функция, полученная на предыдущем шаге.
  • Если n кратно 5, он возвращает функцию, которая добавляет 🐶 к результату вызова функции, полученной с пустой строкой.

После этого вызывается cat с любой из этих функций в качестве аргумента. И опять:

  • Если n не кратно 7, возвращается функция, полученная на предыдущем шаге.
  • Если n кратно 7, он возвращает функцию, которая добавляет 🐱 к результату вызова функции, полученной с пустой строкой.

Наконец, результирующая функция вызывается с n.toString()*.

У меня лихорадка, и единственный рецепт - больше идентификаторов

Со всем этим мы можем сделать последнюю переписку и добавить еще один раунд идентификаторов:

const ಠ_ಠ = n => {
    const test = divisor => str => next =>
        n % divisor ? next : _ => str + next('');
    
    const cat = test(7)('🐱');
    const dog = test(5)('🐶');
    const mouse = test(3)('🐭');
    const id = x => x;
    return cat(dog(mouse(id)))(n.toString());
}

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

Сейчас я дам вам первые 100 значений, но не волнуйтесь, если вы все еще не можете их распознать.

["1", "2", "🐭", "4", "🐶", "🐭", "🐱", "8", "🐭", "🐶", "11", "🐭", "13", "🐱", "🐶🐭", "16", "17", "🐭", "19", "🐶", "🐱🐭", "22", "23", "🐭", "🐶", "26", "🐭", "🐱", "29", "🐶🐭", "31", "32", "🐭", "34", "🐱🐶", "🐭", "37", "38", "🐭", "🐶", "41", "🐱🐭", "43", "44", "🐶🐭", "46", "47", "🐭", "🐱", "🐶", "🐭", "52", "53", "🐭", "🐶", "🐱", "🐭", "58", "59", "🐶🐭", "61", "62", "🐱🐭", "64", "🐶", "🐭", "67", "68", "🐭", "🐱🐶", "71", "🐭", "73", "74", "🐶🐭", "76", "🐱", "🐭", "79", "🐶", "🐭", "82", "83", "🐱🐭", "🐶", "86", "🐭", "88", "89", "🐶🐭", "🐱", "92", "🐭", "94", "🐶", "🐭", "97", "🐱", "🐭", "🐶"]

Здравствуй, темнота, мой старый друг…

За нашим тройным мультяшным животным скрывается не что иное, как наш старый добрый друг FizzBuzz. Мне пришлось немного запутать его, добавив еще один номер для проверки и заменив тезки мертвых раздач на смайлики животных.

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

Я обнаружил, что у него есть два довольно хороших свойства (связанных друг с другом), которые не так просто реализовать с помощью классических императивных реализаций:

  • Проверка, является ли аргумент кратным заданному числу, происходит только один раз. Не только во время каждой ветки выполнения, но и один раз внутри кода.
  • Функциональность очень легко расширяется, когда дело доходит до добавления дополнительных проверок дополнительных чисел, как показано на конкретной показанной реализации.

Это достигается благодаря самосоставляющему характеру функции test, которая имеет следующую сигнатуру типа:

test :: Number -> String -> (String -> String) -> (String -> String)

Это означает, что набор функций test с применением первых двух аргументов (например, mouse, dog и cat) будет свободно комбинироваться друг с другом.

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

*: Если вам интересно, причина, по которой нам нужна строковая версия n, состоит в том, чтобы гарантировать, что функция FizzBuzz всегда возвращает строку. Если ни одна функция test не соответствует, n.toString() проходит через x => x, поэтому это все равно будет число без вызова toString().