Конспект книги Elm in Action
Чтобы купить elm in action дешевле, можно зарегистрироваться на сайте и подождать. Скидки и акции там регулярно.
Конспект elm in action — это заметки, которые я записывал для себя, чтобы не забыть важные моменты. Этот материал отчасти черновик для будущих статей по Elm.
В хронологическом порядке. Версия книги v11.
Qualified и Unqualified доступ к тому, что было импортировано
После такой записи можно вызывать Array в unqualified стиле. Иначе без exposing, для указания типа «массив», вызывали бы в qualified стиле Array.Array
.
Type variable, type alias, custom type
- Type variable — переменная типа, которая используется в аннотациях. Можно применять более одного типа.
- Type alias — имя для типа.
- Custom type — «свой тип»
где PhotoSize
— custom type, а значения — variant
Maybe
Maybe — это контейнер (привет, Профессор Фрисби), который может содержать 1 элемент.
Just value
— Just, это функция. С большой буквы, потому что указывает, что это Custom type + type variable.
Command — это некое значение, которое указывает elm runtime, что необходимо выполнить что-то. В отличии от функции, команда может возвращать разные значения при вызове с теми же аргументами. Функция нет.
Команды — это «сайд эффекты» в Elm.
Обычные же функции, являются чистыми (pure function).
Что будет если вернуть новую модель и команду?
Так как у update возвращаемое значение tuple, можно вернуть сразу и новую модель и команду. В таком случае:
- сначала модель будет изменена
- потом «вьюха» (view) покажет изменения
- затем выполнится команда
Про тип данных tuple, можно найти в документации, в разделе Core language.
В чем разница между Cmd Msg и Cmd msg
(детально на странице 80)
Кратко:
Cmd Msg
— значит, переменная у Cmd должна быть типа MsgCmd msg
— означает, что переменная у Cmd может быть любого типа, так как msg — это type variable (переменная типа, то есть может быть и строкой, и числом и т.д.)
Type «Unit»
()
— это особый тип, называется Unit
, который сразу и тип и значение. Значением типа ()
может быть только ()
. На русский язык, это можно перевести как «ничто». То есть, функция getAnswer
принимает первым аргументом «ничто». Ничего. Значение, которое является ничем.
(Больше информации на стр. 84)
Type List
Обе записи выполняют одно дело: соединяют первый элемент + остальные в List. Но ::
эффективнее, так как не создается экстра-лист в процессе.
…rest и Tuple.pair
Предположим, у нас есть сообщение Loaded (List Photo)
, можно вытащить первый элемент и остальные из листа, с помощью конструкции Loaded (FirstPhoto :: OtherPhotos)
. Это можно назвать :: Pattern (стр. 96) и его аналог из мира JS — …rest.
Данная запись пригодилось в рандом генераторе, используемом через uniform, в который обязательно нужно передать первый элемент. Плюс в коде ниже, интересное использование Tuple.pair для возврата ( model, Cmd Msg )
и |> оператора (называется pipe (труба) оператор):
Type Result
где errValue
и okValue
— type variable, и могут быть любого типа.
Type alias и custom type являются функциями-конструкторами. Есть статья на эту тему — An intro to constructors in Elm
Аргументы нужно передавать в таком же порядке, в котором они указаны в type.
Когда мы хотим обновить какое-то определенное поле в объекте, внутри какой-то структуры, нам до него не достучаться, как привычно в js, нужно создавать доп. переменные, перебирать и ловить с помощью фильтра нужное (как пример), и затем сохранять новое значение. Пример такого подхода, на странице 138 (Table 5.4 Updating the model).
Для порт-функции, которую мы будем использовать в JS, нам неважно, какой msg вернется. Поэтому, еще раз можно обратить внимание на разницу между Cmd Msg и Cmd msg (пункт Table 5.7 Comparing Cmd Msg and Cmd msg.)
И наоборот, когда мы используем подписку (Subscription), нам важен тип сообщения, так как мы будем посылать его в update функцию (пункт Table 5.9 Calling the activityChanges function, стр. 155)
Для того чтобы использовать ports (порты), не забываем указать в начале файла port
перед названием модуля (например: port module PhotoGroove exposing (main)
)
Если нужно вынести общий функционал, который используем при update, то можно сделать функцию (как обычно!), которая вернет model + Cmd Msg или что нужно, в зависимости от задачи. В принципе, тут все как в js, когда мы выносим функцию-хелпер.
requestAnimationFrame
можно использовать rAF, когда нам нужно запустить что-то из JS (например, подписка в порте) «после следующего» апдейта view в DOM. Пример в книге (стр 152, SYNCHRONIZING WITH THE ELM RUNTIME)
Какое поведение уменьшит количество багов?
Asking «which approach rules out more bugs?» is a good way to decide between different ways to model data
Отвечая на вопрос «какое поведение выявит больше багов» можно выбрать среди разных подходов к созданию модели данных.
Техника интересная, направлена на то, чтобы помогать компилятору в будущем помогать нам. То есть, при создании определенной структуры (иногда, будет занимать больше кода) — компилятор будет ругаться, если в процессе рефакторинга мы передадим неверный тип переменной и т.д.
map
Из книги про ФП выяснилось, что есть некая структура «контейнер», и с помощью map мы можем взять значение из этого контейнера, изменить и положить обратно. Например, тип (контейнер!) List, мы взяли, изменили, положили обратно и там у нас так же List.
С остальными структурами такая же история. Но в некоторых случаях, map
может взять контейнер и не изменить в нем ничего.
То есть map
не работает с Err
кейсом у Result
, а только с Ok
. Следовательно значение в контейнере Err
вернется не тронутым. Удобно использовать в тестах.
Подробнее на странице 179 (pretty similar! Each map function…)
Использование .название_свойства
В тоже время, сигатура для .prop
— не просто тип этого свойства, а функция, которая это свойство из объекта вынимает. Пример с тестом:
testSlider "SlidNoise" SlidNoise .noise
Здесь SlidNoise
— строка, далее кастомный тип SlidNoise
, который ждет Int
, и .noise
— количество эффекта в модели.
Тип будет следующим:
(в книге — стр 190, this compiles because the SlidHue variant)
Exposing variants
port module Main exposing (Model, Msg(..), Photo, initialModel)
Если пишем Msg(..)
то значит мы «expose» (выставляем наружу) так же и все варианты (variant) нашего типа Msg
.
Единственный смысл в «expose» просто Msg
, это использование типа в описании, например: Msg -> Model. Так же, здесь можно отметить технику opaque types, которая в книге не упоминалась.
По коду книги, написать Photo(..)
— мы не можем, так как в примере у Photo
нет вариантов.
Тестирование
Документация elm-test
npm install -g elm-test
Команда elm-test init
создаст директорию с тестами и скачает нужные пакет(ы).
Название файла в директории тестов, должно быть таким же, как название тестируемого модуля.
Fuzz тестирование
import Fuzz exposing (Fuzzer, list, int, string)
fuzz позволяет генерировать рандомные значения.
Можно написать свой fuzzer, пример:
Где urlFuzzer
— кастомный fuzzer
.
Тестирование view мне не понравилось. Я бы лучше использовал e2e тестирование на cypress.
Как закинуть данные-рыбу в модель через декодер?
Можно инициировать с помощью Decode.succeed
Constrained Type Variables
comparable, number, appendable
- number, which can resolve to Int or Float
- appendable, which can resolve to String or List
- comparable, which can resolve to Int, Float, Char, String, List or a tuple of these
Если хотим в аннотации к функции хотим использовать разные number, то можно сделать так:
В аннотации (сигнатуре) функции нельзя использовать number, так как выше указаны 3 зарезервированных type variable.
Maybe.andThen
Когда у нас есть два схожих case, которые пытаются обработать Nothing
, мы можем использовать Maybe.andThen
Подробнее: стр. 216-217 (refactoring to use Maybe.andThen)
andThen принимает callback
и Maybe a
значение, а затем возвращает Maybe b
, если в a
не было Nothing
, иначе Nothing
.
Рекурсивный тип
Когда вы указываете type alias, то компилятор «разворачивает» каждый alias в реальный тип и в таком случае нет возможности указать рекурсивный тип (компилятор уйдет в бесконечную рекурсию). На помощь приходит кастомный тип с одним вариантом. Например:
Затем такой variant можно вытащить в одну строку, вместо записи через case of:
Избегаем путаницы в аргументах:
Вместо:
Можно передать record. В таком случае и порядок следования аргументов не важен (в JS это передача в функцию объекта со свойствами, а не аргументов через запятую):
Циклическая зависимость в декодере
Для рекурсивных декодеров пригодится Decoder.lazy, так как он остановит компилятор от бесконечной попытки «раскрыть» тип декодера:
Reduce aka Foldl(r)
Foldl — тоже самое, что reduce в JS, foldr — reduceRight
foldl и foldr есть у большинства структур, не только у List.
Dict.union
Если нужно объединить данные в один dict без дублирования ключей — dict.union наш выбор.
If there is a collision, preference is given to the first dictionary.
Если ключ будет присутствовать в первом словарике и во втором, то предпочтение будет отдано первому. То есть, значение из первого словаря будет в финальном словаре (Dict).
Избегаем нежелательных ре-рендеров
Html.lazy может помочь. Подробнее в документации и в книге в разделе 8.1.3 Skipping Unnecessary Renders with Html.Lazy
Похоже на shouldComponentUpdate из react: указываем изменение какого параметра в модели нам интересно и все остальное будет игнорироваться.
Роутинг
- страница 264, начинается про работу с URL
- страница 266 — рассказ про url с параметром:
Если бы было (s "photos" "car")
, то это был бы не параметризированный url-адрес «photos/car».
Список урлов — Parser.oneOf
В oneOf у парсера мы можем держать список всех доступных URL-адресов приложения.
Truth table pattern
Паттерн называется — «таблица правды» (truth table) и его цель — перечислить все возможные варианты.
Ниже пример, который показывает состояние ссылки — активная или нет, в зависимости от страницы. В приложении есть страницы и «подстраницы», поэтому в таблице как раз указаны возможные комбинации.
Html.map
(стр 281)
Вопрос, который долго меня мучал как новичка — в чем разница между Html Msg
у родителя и Html Msg
у ребенка.
Так как книга подходит к концу, суть уже ясна: модуль Main
имеет свой тип Msg
, и поэтому когда мы вызываем дочерний_модуль.view
— то функция возвращает Html Msg
дочернего модуля. Для модуля Main
возвращаемое значение будет уже типа Html Children.Msg
.
Как итог, получаем несоответствие типов, так как у Main
view
сигнатура: Model -> Html Msg
Как быть? Как всегда — с помощью .map
превращаем из А в Б.
Итого:
В процессе работы над Elm in action, получается приложение близкое к фотогалерее и фоторедактору, очень упрощенным, конечно. Тем не менее, в книге очень хорошо описаны основные понятия и, как говорит сам Ричард, нет умных слов: монады, теория категорий и прочее…
Рекомендую, мне книга очень понравилась.
Итоговый код можно посмотреть здесь.