Пары
Обратите внимание на то, что пары неизменяемы. Нельзя просто так взять и изменить пару. Можно только создать новую на основе предыдущей. Поначалу такой способ программирования может показаться необычным и сложным, так как надо перестроить свое мышление. Чем дальше вы будете продвигаться по курсам, тем больше он вам начнет нравиться. Вы увидите, как часто упрощается код и его отладка в отсутствие изменяемости.
Ошибки
Работая с парами, очень легко допустить ошибку, которая будет выглядеть так:
Argument must be a pair, but it was ...
Парой является только то, что создано с помощью конструктора cons
. Если по какой-то причине в селекторы произошла передача не пары, то результатом будет как раз такая ошибка. Проверить это очень легко:
const pair = 3
car(pair) // Argument must be a pair, but it was '3'
То же самое, если передать селектору строку вместо пары:
const pair = 'hello'
car(pair) // Argument must be a pair, but it was 'hello'
Конспект урока
Мы уже написали несколько полезных функций для работы с точками и, в принципе, поняли, как работает эта абстракция. Теперь пришло время копнуть на уровень глубже и посмотреть, как же устроены наши точки.
import { cons, car, cdr } from 'hexlet-pairs'
// Конструктор
const pair = cons(8, 7)
car(pair) // 8
cdr(pair) // 7
const pair2 = cons(3, pair)
Устроены они достаточно просто и используют структуру данных, которая называется парой. Пар в самом языке JavaScript не существует, мы их реализовали с помощью отдельной библиотеки, и выше можно увидеть пример того, как они используются. Мы импортируем из библиотеки конструктор cons
и селекторы car
и cdr
. Конструктор создает пару, а селекторы служат для извлечения из пары первого значения (с помощью car
) и второго значения (с помощью cdr
). Все достаточно просто и очень похоже на реализацию точек из прошлого урока.
Что интересно, элементами пары могут быть другие пары. В будущем это даст нам очень мощные возможности для того, чтобы строить более сложные структуры данных, в том числе списковые.
import { cons, car, cdr, toString } from '@hexlet/pairs'
const pair = cons('first', 'second')
console.log(car(pair)) // => first
console.log(cdr(pair)) // => second
// don't do it
console.log(pair) // => { [Function] pair: true }
console.log(toString(pair)) // => (first, second)
Давайте посмотрим, как представлены наши точки с помощью пар:
import { cons, car, cdr } from 'hexlet-pairs'
const makePoint = (x, y) => cons(x, y)
const getX = point => car(point)
const getY = point => cdr(point)
const toString = point => String(point)
Здесь все предельно просто: makePoint
— это функция, которая принимает x
и y
и вызывает конструктор пары с этими аргументами. То же самое с селекторами: getX
и getY
принимают на вход точку и вызывают с этой точкой car
и cdr
соответственно.
Можно заметить, что сработало бы даже такое определение:
const makePoint = cons
const getX = car
const getY = cdr
Здесь все верно с синтаксической точки зрения и с точки зрения получения конечного результата. Но с таким способом определения есть некоторые проблемы: по сути, когда мы делаем такое присваивание, получается, что makePoint
и cons
являются одним и тем же объектом. Кто-то может сказать, что они ссылаются на одну и ту же функцию, но это уже тонкости реализации конкретного языка программирования. На практике это означает, что, запустив построенный таким образом код, вы не увидите вызова функций makePoint
, getX
или getY
, потому что их фактически не существует. При отладке вы не найдете этих функций в трассировке стека. Вы можете захотеть увидеть все вызовы, например, makePoint
, но вы точно не захотите отслеживать в вашей программе все вызовы cons
, которые могут использоваться не только для точек, а вообще для любых библиотек. Поэтому мы не используем такое определение, но о нем нужно знать, чтобы понимать, как в целом все работает.
Теперь, используя пары, мы можем строить новые абстракции, расширяя нашу библиотеку графических примитивов. Мы вводим понятие отрезка, для которого мы создаем конструктор и селекторы:
const point1 = makePoint(1, 2)
const point2 = makePoint(10, -2)
const segment = makeSegment(point1, point2)
startSegment(segment) // (1, 2)
endSegment(segment) // (10, -2)
Нам нужно сделать две точки (потому что любой отрезок представлен двумя точками). После этого мы используем конструктор makeSegment
и передаем туда наши точки, а с помощью селекторов startSegment
и endSegment
мы получаем точки. Важно, что мы получаем именно точки, потому что это тоже составные данные со своими селекторами, с помощью которых можно получать примитивные значения и производить над ними какие-либо манипуляции при необходимости.
Дополнительные материалы
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.