Сигнатура типов функции в JavaScript

Roman Ponomarev
devSchacht
Published in
6 min readJun 14, 2017

Перевод статьи Functional Reactive Ninja: Function Type Signatures in Javascript.

Когда разработчик Javascript начинает познавать самые глубокие секреты функционального программирования, он часто встречает эти странные стрелки с типом, написанные над функциями, и думает: «Что за черт?». В конце концов, он мастер динамически типизированного Javascript, свободный от ограничений типов.

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

Скриншот из ramdajs

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

Сигнатура типов определяет входящие и возвращаемые типы для функции, иногда включая число аргументов, типы аргументов и порядок аргументов, содержащихся в функции.

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

Сигнатуры типов основаны на системе типов Хиндли-Милнера как стандартной системе типов для языков ML, включая Haskell.

Эти высказывания служат великой цели формализации функционального выражения в алгоритмах Type Inferring (широко распространены в Haskell), но пока мы будем использовать их для более качественного документирования нашего кода Javascript и получения из него произвольных теорем.

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

Прям как Хи-Мен

Мы будем создавать сигнатуры типов как комментарии над нашими функциями. Вы также можете использовать Flow для вывода типов при использовании функций. Можете начать знакомство с Flow здесь:

Простые функции

// length :: String → Number
const length = s => s.length;

Вышеуказанная функция принимает строку и возвращает число. Если мы посмотрим внимательно, мы увидим:

  1. Сначала записывается имя функции, а затем ::.
  2. Входящий тип записывается перед стрелкой.
  3. Возвращаемый тип записывается после стрелки или в самом конце.

Помните, что записываются только входящие и возвращаемые типы, так что высказывание можно прочитать вот так: «Функция length от строки до числа».

Вышеупомянутая функция length также может быть записана как:

// length :: [Number] → Number
const length = arr => arr.length

И это нормально, чтобы функция имела множественные сигнатуры, пока это удобно. Если функция становится слишком гибкой из-за типов своего параметра, тогда мы должны использовать произвольные переменные Хиндли-Милнера — мы обсудим их ниже.

Несколько параметров

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

// join :: (String, [String]) → String
const join = (separator, arr) => arr.join(separator)

Функции высшего порядка

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

// addOneToAll :: ((Number → Number),[Number]) → [Number]
const addOneToAll = (addOne = x=>x+1 , arr) => arr.map(addOne)

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

Вышеупомянутая функция является функцией «map», и она не работает только с конкретными типами данных: она может работать с любым типом массива. Поэтому для описания таких функций нам нужно что-то еще.

Произвольные переменные Хиндли-Милнера

Такие функции, как identity, map, filter и reduce, принимают аргументы, являющиеся слишком гибкими, чтобы определяться конкретным типом, поэтому мы используем классические переменные Хиндли-Милнера a и b

// identity :: a → a
const identity = a => a

Поскольку identity всегда будет давать нам тот же возвращаемый тип для одного и того же входящего типа, мы использовали a → a для представления его сигнатуры.

Я знаю!

Также нашу функцию length можно записать так:

// length :: [a] → Number
const length = arr => arr.length

По аналогии:

// head :: [a] → a
const head = arr => arr[0]

Thunks или каррированные функции

Сигнатуры типов самых чистых из чистых функций✨

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

Если вам интересно, почему мы должны каррировать наши функции, перейдите сюда:

// map :: (a → b) → [a] → [b]
const map = fn => arr => arr.map(fn)

Стандартная функция map будет иметь указанную выше сигнатуру типов. Но также можно встретить map с такой сигнатурой типа:

map :: [a] → [b]

Иногда мы знаем тип массива, возвращаемого map, как в этом случае.

// allToString :: [a] → [String]
const allToString = arr => arr.map(toString)

Давайте посмотрим на стандартные filter и reduce

// filter :: (a → bool) → [a] → [a]
const filter = fn => arr => arr.filter(fn)
// reduce :: (b → a → b) → b → [a] → b
const reduce = fn => init => arr => arr.reduce(fn, init)

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

Итак, первый аргумент reduce - это функция уменьшения, принимающая b и a, чтобы вернуть b. Это означает, что функция будет уменьшать все в тип b, поэтому конечное значение, полученное из reduce() и предоставленное начальное значение (init), будут иметь значение типа b. И так как каждое отдельное значение из списка типа a будет проходить через эту функцию уменьшения, поэтому второй аргумент функции уменьшения должен быть типа a. Поэтому такая сигнатура типов reduce() является оправданной.

Произвольные теоремы

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

// Сигнатура типов head следующая:
// head :: [a] → a
compose(map(fn), head) == compose(head, fn)

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

Докажем эту теорему:

compose(map(fn), head) == compose(head, fn)--Переводим в сигнатуры типов--[a] → [b] → b == [a] → a → b-- Убираем промежуточные этапы --[a] → b == [a] → b

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

Вышеприведенный вывод упрощен, так как для настоящего вывода произвольных теорем потребуются лямбда-вычисления, объяснение которых не является целью данной статьи.

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

Обратите внимание, что функция сompose, используемая здесь, фактически противоположна идиоматическому compose. Больше информации здесь.

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

Спасибо за прочтение 💖

Слушайте наш подкаст в iTunes и SoundCloud, читайте нас на Medium, контрибьютьте на GitHub, общайтесь в группе Telegram, следите в Twitter и канале Telegram, рекомендуйте в VK и Facebook.

Статья на GitHub

--

--