Перевод статьи Tom Harding: Fantas, Eel, and Specification 1: Daggy. Опубликовано с разрешения автора.
Ещё раз привет, Интернет! Как фанатик функционального программирования* и JavaScript разработчик†, я провожу много времени бредя об их скрещивании. В этой серии мы будем смотреть на спецификацию Fantasy Land в её полном объёме и пройдемся по примерам, как мы можем использовать типы классов в ней. Однако, прежде чем мы пойдем дальше, нам нужно поговорить о daggy
.
Daggy — это крошечная библиотека для создания суммы типов для функционального программирования. Не беспокойтесь слишком много о том, что это означает, и сосредоточьтесь на двух функциях, которые экспортирует библиотека: tagged
и taggedSum
.
Это очень простой способ для создания типов с одним конструктором. Другими словами, думайте о нем, как о способе для хранения данных с жёсткой структурой (например, моделей):
//- Координата в 3D пространстве.
//+ Coord :: (Int, Int, Int) -> Coord
const Coord = daggy.tagged('x', 'y', 'z')
//- Линия между двумя координатами.
//+ Line :: (Coord, Coord) -> Line
const Line = daggy.tagged('from', 'to')
Результирующая структура довольно понятна:
// Мы можем добавить методы...
Coord.prototype.translate =
function (x, y, z) {
// Именованные свойства!
return Coord(
this.x + x,
this.y + y,
this.z + z
)
}
// Автоматически заполнить именованные свойства
const origin = Coord(0, 0, 0)
const myLine = Line(
origin,
origin.translate(2, 4, 6)
)
В этом нет ничего страшного, если вы до этого использовали систему объектов в JavaScript: всё что на самом деле даёт функция tagged
— заполняет именованные свойства в объекте. Это всё. Маленькая утилита для создания конструкторов с именованными свойствами.
Теперь для заинтересованных. Подумайте о булевом типе: у него есть два значения: True
и False
. Для того, чтобы представлять такую структуру как Bool
, нам нужно сделать тип с несколькими конструкторами (то, что мы называем сумма типов):
const Bool = daggy.taggedSum({
True: [], False: []
})
Мы вызываем разные формы типов через конструктор типов: в данном случае, это True
и False
, и они не имеют никаких аргументов. Что если мы возьмём наш код из примера tagged
и создадим более сложный тип?
const Shape = daggy.taggedSum({
// Square :: (Coord, Coord) -> Shape
Square: ['topleft', 'bottomright'],
// Circle :: (Coord, Number) -> Shape
Circle: ['centre', 'radius']
})
В отличии от логических значений, наши конструкторы здесь имеет значения. Они принимают различные значения, в зависимости от используемого конструктора, но мы знаем что Square
и Circle
наверняка оба конструкторы типа Shape
. Как это поможет нам?
Shape.prototype.translate =
function (x, y, z) {
return this.cata({
Square: (topleft, bottomright) =>
Shape.Square(
topleft.translate(x, y, z),
bottomright.translate(x, y, z)
),
Circle: (centre, radius) =>
Shape.Circle(
centre.translate(x, y, z),
radius
)
})
}
Square(Coord(2, 2, 0), Coord(3, 3, 0))
.translate(3, 3, 3)
// Square(Coord(5, 5, 3), Coord(6, 6, 3))
Circle(Coord(1, 2, 3), 8)
.translate(6, 5, 4)
// Circle(Coord(7, 7, 7), 8)
Как и прежде, мы определяем методы на прототипе Shape
. Однако Shape
не конструктор, это тип: Shape.Square
и Shape.Circle
конструкторы.
Это означает, что когда мы пишем метод, мы должны писать то, что будет работать для всех форм типа Shape
и this.cata
— это киллер фича Daggy. Кстати, cata сокращение для catamorphism!
Все что мы делаем, это пробрасываем объект { constructor: handler }
в функцию cata
и соответсвующий конструктор будет вызван, когда метод выполнится. Как мы можем видеть выше, теперь у нас есть метод translate
, который будет работать для обоих типов Shape
!
Мы можем даже определить метод для нашего типа Bool
:
const { True, False } = Bool
// Меняем местами логические значения.
Bool.prototype.invert = function () {
return this.cata({
False: () => True,
True: () => False
})
}
// Сокращение для Bool.prototype.cata?
Bool.prototype.thenElse =
function (then, or) {
return this.cata({
True: then,
False: or
})
}
Как видите, для конструкторов без аргументов, мы используем обработчики без аргументов. Также обратите внимание, что различные конструкторы одной суммы типов могут иметь совершенно разное число и типы аргументов. Это будет очень важно, когда мы перейдем к примерам структур из Fantasy Land.
Это все, что нужно знать о taggedSum
: она позволяет нам создавать типы с несколькими конструкторами и удобно писать методы для них.
Как последний пример taggedSum
(потому что я надеюсь, что с tagged
всё ясно и понятно), вот связанный список и пара полезных функций:
const List = daggy.taggedSum({
Cons: ['head', 'tail'], Nil: []
})
List.prototype.map = function (f) {
return this.cata({
Cons: (head, tail) => List.Cons(
f(head), tail.map(f)
),
Nil: () => List.Nil
})
}
// "Статичный" метод для удобства.
List.from = function (xs) {
return xs.reduceRight(
(acc, x) => List.Cons(x, acc),
List.Nil
)
}
// И обратное преобразование для удобства!
List.prototype.toArray = function () {
return this.cata({
Cons: (x, acc) => [
x, ... acc.toArray()
],
Nil: () => []
})
}
// [3, 4, 5]
console.log(
List.from([1, 2, 3])
.map(x => x + 2)
.toArray())
Конечно, мы можем создать список с двумя конструкторами, Cons
и Nil
(как мы сделали с [x, ... xs]
и []
в моем последнем посте), и каждый объект списка будет иметь соответствующий объект массива‡. Например, [1, 2, 3]
станет Cons(1, Cons(2, Cons(3, Nil)))
, так что это довольно очевидно, как любой список может быть переведён!
Это все, что нужно знать о daggy
, чтобы понять Fantasy Land! Если вы хотите закрепить ваше понимание, почему бы не попробовать добавить ещё пару функций массива к типу List
, таких как filter
или reduce
?
В противном случае, у нас есть ещё одна вещь, о которой стоит поговорить, до того как мы приступим к структурам: описание типа!
А пока, берегите себя! ♥
* Моё (дословно) представление Дэном членам основной команды разработки PHP.
† Даже если только формально.
‡ Мы называем этот изоморфизм!
Читайте нас на Medium, контрибьютьте на Github, общайтесь в группе Telegram, следите в Twitter и канале Telegram, рекомендуйте в VK и Facebook. Скоро подъедет подкаст, не теряйтесь.
В оригинальном названии статьи используется непереводимая игра слов, основанная на схожести звучания названия спецификации Fantasy Land