Конспект книги 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 — переменная типа, которая используется в аннотациях. Можно применять более одного типа.

где PhotoSizecustom 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 должна быть типа Msg
  • Cmd 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 и okValuetype 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

  1. number, which can resolve to Int or Float
  2. appendable, which can resolve to String or List
  3. 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, foldrreduceRight

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

Рекомендую, мне книга очень понравилась.

Итоговый код можно посмотреть здесь.

Макс

Recent Posts

Elm #2: Загрузка и отображение json

Сегодня будем использовать parcel и IntelliJ IDEA Community Edition. Все инструменты бесплатные. Инициализация elm проекта…

5 лет ago

Elm для React/Redux разработчиков (Elm #1: Знакомимся с Elm)

На данном вебинаре мы знакомились с языком Elm проводя параллели между Elm и Redux, поэтому…

5 лет ago

Масштабируем Elm-приложение (конспект)

Richard Feldman рассказывает как масштабировать Elm приложение без боли. Показаны техники: extended records, подход narrow…

5 лет ago

Elm видео, за ноябрь 2019

В данной заметке вы найдете конспект видео по Elm, которые я посмотрел в ноябре 2019.…

5 лет ago

Итоги 2019. Что учить фронтендеру в 2020?

Итоги года 2019 // Max Frontend Покажи мне свой гитхаб, и я скажу работал ли…

5 лет ago

Почему мне стоит изучать Elm?

Почему стоит изучать Elm? Потому что это интересный вызов, редкие (но вкусные) вакансии и хороший…

5 лет ago