Cпецификация волшебного мира 1: Daggy

Vadim Yalo
Apr 11, 2017 · 4 min read

Перевод статьи Tom Harding: Fantas, Eel, and Specification 1: Daggy. Опубликовано с разрешения автора.

Еще раз привет, Интернет! Как фанатик функционального программирования* и JavaScript разработчик†, я провожу много времени бредя об их скрещивании. В этой серии мы будем смотреть на спецификацию Fantasy Land в её полном объеме и пройдемся по примерам, как мы можем использовать типы классов в ней. Однако, прежде чем мы пойдем дальше, нам нужно поговорить о daggy.

Daggy — это крошечная библиотека для создания суммы типов для функционального программирования. Не беспокойтесь слишком много о том, что это означает, и сосредоточьтесь на двух функциях, которые экспортирует библиотека: tagged и taggedSum.

daggy.tagged(... fields)

Это очень простой способ для создания типов с одним конструктором. Другими словами, думайте о нем, как о способе для хранения данных с жёсткой структурой (например, моделей):

//- Координата в 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 — заполняет именованные свойства в объекте. Это всё. Маленькая утилита для создания конструкторов с именованными свойствами.

daggy.taggedSum(constructors)

Теперь для заинтересованных. Подумайте о булевом типе: у него есть два значения: 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: она позволяет нам создавать типы с несколькими конструкторами и удобно писать методы для них.

List, но не меньший…

Как последний пример 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. Скоро подъедет подкаст, не теряйтесь.

Статья на Github


В оригинальном названии статьи используется неперводимая игра слов, основанная на схожести звучания названия спецификации Fantasy Land

devSchacht

Подкаст. Переводы. Веб-разработка.

Vadim Yalo

Written by

devSchacht

Подкаст. Переводы. Веб-разработка.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade