Спецификация Static Land
Перевод спецификации Static Land.
Это спецификация для общих алгебраических типов в JavaScript на основе спецификации Fantasy Land.
Отличие от Fantasy Land
Fantasy Land использует методы как основу для типов. Экземпляр типа в Fantasy Land это объект с определенными методами. Например, экземпляр типа Functor должен быть объектом, у которого есть метод map
.
В Static Land тип — просто набор статических функций и экземпляром типа может быть любое значение, в том числе примитивы (Number, Boolean, и т.д.)
Например, мы можем реализовать тип Addition, который использует числа как экземпляры и удовлетворяет закону моноид:
const Addition = { empty() {
return 0
}, concat(a, b) {
return a + b
},}
Плюсы
- Нет конфликтов имен. Поскольку тип — это просто набор функций, которые не имеют общих имен, у нас нет проблем с совпадением имен.
- Мы можем реализовать много типов для того же значения. Например, мы можем реализовать два Monoid’a для чисел: Addition и Multiplication.
- Мы можем реализовать тип со значением в виде примитива (Number, Boolean, и т.д.).
- Мы можем реализовать бесшовный тип. Например, мы можем реализовать тип со значением в виде массива и пользователю не надо будет оборачивать/разворачивать значения в классы обёртки с методами Fantasy Land.
Минусы
- Нам придётся пробрасывать типы чаще. В Fantasy Land какой-то обычный код может быть написан с использованием только методов, мы должны прокинуть только
of
иempty
. В Static Land мы должны прокидывать типы для любого кода.
Как добавить совместимость с Static Land для вашей библиотеки
Просто раскройте некоторые Типы, которые работают с типами, которые предоставляет ваша библиотека или с типами объявленными в другой библиотеке или с нативными типам, такими как Array.
Тип не должны быть простыми объектами JavaScript; они также могут быть конструкторами при желании. Единственным требованием является:
- этот объект содержит несколько статических методов из Static Land; и
- если он содержит метод с именем как в Static Land, то этот метод должен быть методом Static Land (подчиняясь закону и т.д.).
Пример 1. Тип Static Land для Array
const SArray = { of(x) {
return [x]
}, map(fn, arr) {
return arr.map(fn)
}, chain(fn, arr) {
// ...
},}export {SArray}
Пример 2. Тип Static Land как Class
class MyType = { constructor() {
// ...
} someInstanceMethod() {
// ...
} static someNonStaticLandStaticMethod() {
// ...
}
// Static Land methods static of(x) {
// ...
} static map(fn, value) {
// ...
}}export {MyType}
Тип
Тип в Static Land это словарь (JavaScript объект) со статическими функциями в качестве значений. ‘Статический’ означает что функции не используют this
, они могут быть убраны у объекта типа. Объект типа просто контейнер для функций.
const {of, map} = MyType// Должно работать
map(x => x + 1, of(41)) // MyType(42)
Функции из объекта типа часто называются “методами” типа. Но запомните что они не методы в JS представлении (они не используют this
).
Описание типа
Каждый метод в этой спецификации имеет описание типа, оно выглядит так.
map :: Functor f => Type f ~> (a → b, f a) → f b
Мы используем синтаксис похожий на Хаскель. Вы можете узнать о нём из вики Ramda’ы или из книги “В основном адекватное руководство профессора Фрисби по функциональному программированию”.
Эта спецификация использует расширения для синтаксиса описания типов:
(a, b) → c
обозначает булеву функцию, которая не каррирована. Тоже самое для большего числа аргументов.Type a
обозначает типизированный словарь типаa
. Например, функция с описанием(Type f, f a) → f a
может быть вызвана такfn(F, F.of(1))
.~>
обозначает доступ к свойствам JavaScript объекта. Например,fn :: Type f ~> (f a) → f a
может быть применена такF.fn(F.of(1))
.
Если метод вызывается с неправильными типами, поведение неопределенно. Также, если метод принимает функцию, он должен применять функцию только в соответствии с описанием типа, т.е. обеспечивать правильное количество аргументов и правильные типы.
Параметризация
Все реализации методов должны использовать только информацию о типе аргумента, известную из описания типа метода. не разрешено проверять аргументы или значения, которые они возвращают или содержат для получения большей информации о их типах. Другими словами методы должны быть параматрически полиморфны.
Например, давайте рассмотрим описание метода функтора map
:
map :: Functor f => Type f ~> (a → b, f a) → f b
В нём есть три типа переменных: f
, a
, and b
. Также у нас есть некоторые ограничения:
Functor f
говорит чтоf a
— значение Functor.Type f ~>
означает, что мы пишем реализациюmap
для типизированного словаряType f
. В этом случае мы знаем, что определенный типType f
работает с методом, поэтому мы знаем все оf
.
У нас нет никаких ограничений для типов a
и b
, так что мы ничего не знаем о них. И нам нельзя проверять их.
Вот реализация Maybe, которая нарушает требование параметризации, хотя вписывается в описание типа:
Maybe.Nothing = {type: 'Nothing'}Maybe.of = x => {
if (x === undefined) { // проверка не разрешена
return Maybe.Nothing
}
return {type: 'Just', value: x}
}Maybe.map = (f, v) => { // это законная проверка `f a`, потому что мы знаем структуру `f`
if (v.type === 'Nothing') {
return v
} const a = v.value
const b = f(a) if (b === undefined) { // проверка не разрешена
return Maybe.Nothing
} return Maybe.of(b)
}
Эквивалентность
Эквивалентности для данного значения должна соответствовать определению, что два значения могут быть безопасно поменяны местами в программе, что подтверждает абстракцию.
Например:
- Два списка эквивалентны, если они эквивалентны по всем показателям.
- Два обычных JavaScript объекта, представленные как словари, эквивалентны, когда они эквивалентны по всем ключам.
- Два промиса эквивалентны, когда они возвращают эквивалентные значения.
- Две функции эквивалентны, если они дают эквивалентные результаты для эквивалентных входных данных.
мы используем символ ≡
в правилах для обозначения эквивалентности.
Алгебры
Алгебра представляет собой набор значений(экземпляров типа и других значений), набор операторов(методов типа), которые зависимы и должны подчиняться некоторым правилам.
Каждая алгебра — это отдельная спецификация. Алгебра может иметь зависимости от реализаций других алгебр.
Алгебра также может содержать другие методы алгебр, которые могут быть получены из новых методов. Если тип имеет метод, который может быть получен, его поведение должно быть эквивалентно тому, из которого он получен.
- Setoid
- Semigroup
- Monoid
- Functor
- Bifunctor
- Contravariant
- Profunctor
- Apply
- Applicative
- Alt
- Plus
- Alternative
- Chain
- ChainRec
- Monad
- Foldable
- Extend
- Comonad
- Traversable
Setoid
Методы
equals :: Setoid s => Type s ~> (s, s) → Boolean
Правила
- Рефлексивность:
S.equals(a, a) === true
- Симметрия:
S.equals(a, b) === S.equals(b, a)
- Транзитивность: если
S.equals(a, b)
иS.equals(b, c)
, тогдаS.equals(a, c)
Semigroup
Методы
concat :: Semigroup s => Type s ~> (s, s) → s
Правила
- Ассоциативность:
S.concat(S.concat(a, b), c) ≡ S.concat(a, S.concat(b, c))
Monoid
Зависимости
- Semigroup
Методы
empty :: Monoid m => Type m ~> () → m
Правила
- Точный справа:
M.concat(a, M.empty()) ≡ a
- Точный слева:
M.concat(M.empty(), a) ≡ a
Functor
Методы
map :: Functor f => Type f ~> (a → b, f a) → f b
Правила
- Точный:
F.map(x => x, a) ≡ a
- Композиция:
F.map(x => f(g(x)), a) ≡ F.map(f, F.map(g, a))
Bifunctor
Зависимости
- Functor
Методы
bimap :: Bifunctor f => Type f ~> (a → b, c → d, f a c) → f b d
Правила
- Точный:
B.bimap(x => x, x => x, a) ≡ a
- Композиция:
B.bimap(x => f(g(x)), x => h(i(x)), a) ≡ B.bimap(f, h, B.bimap(g, i, a))
Может быть получен
- Functor’s map:
A.map = (f, u) => A.bimap(x => x, f, u)
Contravariant
Методы
contramap :: Contravariant f => Type f ~> (a → b, f b) → f a
Правила
- Точный:
F.contramap(x => x, a) ≡ a
- Композиция:
F.contramap(x => f(g(x)), a) ≡ F.contramap(g, F.contramap(f, a))
Profunctor
Зависимости
- Functor
Методы
promap :: Profunctor f => Type f ~> (a → b, c → d, f b c) → f a d
Правила
- Точный:
P.promap(x => x, x => x, a) ≡ a
- Композиция:
P.promap(x => f(g(x)), x => h(i(x)), a) ≡ P.promap(g, h, P.promap(f, i, a))
Может быть получен
- Functor’s map:
A.map = (f, u) => A.promap(x => x, f, u)
Apply
Зависимости
- Functor
Методы
ap :: Apply f => Type f ~> (f (a → b), f a) → f b
Правила
- Композиция:
A.ap(A.ap(A.map(f => g => x => f(g(x)), a), u), v) ≡ A.ap(a, A.ap(u, v))
Applicative
Зависимости
- Apply
Методы
of :: Applicative f => Type f ~> a → f a
Правила
- Точный:
A.ap(A.of(x => x), v) ≡ v
- Гомоморфизм:
A.ap(A.of(f), A.of(x)) ≡ A.of(f(x))
- Перестановка:
A.ap(u, A.of(y)) ≡ A.ap(A.of(f => f(y)), u)
Может быть получен
- Functor’s map:
A.map = (f, u) => A.ap(A.of(f), u)
Alt
Зависимости
- Functor
Методы
alt :: Alt f => Type f ~> (f a, f a) → f a
Правила
- Ассоциативность:
A.alt(A.alt(a, b), c) ≡ A.alt(a, A.alt(b, c))
- Распределённость:
A.map(f, A.alt(a, b)) ≡ A.alt(A.map(f, a), A.map(f, b))
Plus
Зависимости
- Alt
Методы
zero :: Plus f => Type f ~> () → f a
Правила
- Точный справа:
P.alt(a, P.zero()) ≡ a
- Точный слева:
P.alt(P.zero(), a) ≡ a
- Упразднение:
P.map(f, P.zero()) ≡ P.zero()
Alternative
Зависимости
- Applicative
- Plus
Методы
- Распределённость:
A.ap(A.alt(a, b), c) ≡ A.alt(A.ap(a, c), A.ap(b, c))
- Упразднение:
A.ap(A.zero(), a) ≡ A.zero()
Chain
Зависимости
- Apply
Методы
chain :: Chain m => Type m ~> (a → m b, m a) → m b
Правила
- Ассоциативность:
M.chain(g, M.chain(f, u)) ≡ M.chain(x => M.chain(g, f(x)), u)
Может быть получен
- Apply’s ap:
A.ap = (uf, ux) => A.chain(f => A.map(f, ux), uf)
ChainRec
Зависимости
- Chain
Методы
chainRec :: ChainRec m => Type m ~> ((a → c, b → c, a) → m c, a) → m b
Правила
- Эквивалентность:
C.chainRec((next, done, v) => p(v) ? C.map(done, d(v)) : C.map(next, n(v)), i) ≡ (function step(v) { return p(v) ? d(v) : C.chain(step, n(v)) }(i))
- Использование
C.chainRec(f, i)
должно быть максимально подобным самостоятельному вызовуf
.
Monad
Зависимости
- Applicative
- Chain
Правила
- Точный слева:
M.chain(f, M.of(a)) ≡ f(a)
- Точный справа:
M.chain(M.of, u) ≡ u
Может быть получен
- Functor’s map:
A.map = (f, u) => A.chain(x => A.of(f(x)), u)
Foldable
Методы
reduce :: Foldable f => Type f ~> ((a, b) → a, a, f b) → a
Правила
F.reduce ≡ (f, x, u) => F.reduce((acc, y) => acc.concat([y]), [], u).reduce(f, x)
Extend
Методы
extend :: Extend e => Type e ~> (e a → b, e a) → e b
Правила
- Associativity:
E.extend(f, E.extend(g, w)) ≡ E.extend(_w => f(E.extend(g, _w)), w)
Comonad
Зависимости
- Functor
- Extend
Методы
extract :: Comonad c => Type c ~> c a → a
Правила
C.extend(C.extract, w) ≡ w
C.extract(C.extend(f, w)) ≡ f(w)
C.extend(f, w) ≡ C.map(f, C.extend(x => x, w))
Traversable
Зависимости
- Functor
- Foldable
Методы
traverse :: (Traversable t, Applicative f) => Type t ~> (Type f, (a → f b), t a) → f (t b)
Правила
- Нормализованность:
f(T.traverse(A, x => x, u)) ≡ T.traverse(B, f, u)
для любогоf
такого чтоB.map(g, f(a)) ≡ f(A.map(g, a))
- Точный:
T.traverse(F, F.of, u) ≡ F.of(u)
для любого ApplicativeF
- Композиция:
T.traverse(ComposeAB, x => x, u) ≡ A.map(v => T.traverse(B, x => x, v), T.traverse(A, x => x, u))
дляComposeAB
определённого ниже и для любого ApplicativesA
иB
const ComposeAB = { of(x) {
return A.of(B.of(x))
}, ap(a1, a2) {
return A.ap(A.map(b1 => b2 => B.ap(b1, b2), a1), a2)
}, map(f, a) {
return A.map(b => B.map(f, b), a)
},}
Может быть получен
reduce
метод Foldable:
F.reduce = (f, acc, u) => {
const of = () => acc
const map = (_, x) => x
const ap = f
return F.traverse({of, map, ap}, x => x, u)
}
map
метод Functor:
F.map = (f, u) => {
const of = (x) => x
const map = (f, a) => f(a)
const ap = (f, a) => f(a)
return F.traverse({of, map, ap}, f, u)
}
Слушайте наш подкаст в iTunes и SoundCloud, читайте нас на Medium, контрибьютьте на GitHub, общайтесь в группе Telegram, следите в Twitter и канале Telegram, рекомендуйте в VK и Facebook.