Начало работы с языком программирования Elm

Опубликовано: 2022-03-11

Когда ведущий разработчик очень интересного и инновационного проекта предложил перейти с AngularJS на Elm, моей первой мыслью было: почему?

У нас уже было хорошо написанное приложение AngularJS, которое было в надежном состоянии, хорошо протестировано и проверено в производстве. И Angular 4, будучи достойным обновлением AngularJS, мог бы стать естественным выбором для переписывания, как и React или Vue. Elm казался каким-то странным предметно-ориентированным языком, о котором люди едва слышали.

язык программирования вяз

Ну, это было до того, как я что-то узнал об Элме. Теперь, имея некоторый опыт работы с ним, особенно после перехода на него с AngularJS, я думаю, что могу дать ответ на это «почему».

В этой статье мы рассмотрим плюсы и минусы Elm и то, как некоторые из его экзотических концепций идеально подходят для нужд фронтенд-разработчика. Статью о языке Elm, более похожую на учебник, вы можете найти в этом сообщении в блоге.

Elm: чисто функциональный язык программирования

Если вы привыкли программировать на Java или JavaScript и считаете, что это естественный способ написания кода, изучение Elm может быть похоже на падение в кроличью нору.

Первое, что вы заметите, это странный синтаксис: никаких фигурных скобок, много стрелок и треугольников.

Вы можете научиться жить без фигурных скобок, но как вы определяете и вкладываете блоки кода? Или, где цикл for или любой другой цикл, если уж на то пошло? Хотя явная область действия может быть определена с помощью блока let , здесь нет блоков в классическом смысле и вообще нет циклов.

Elm — чисто функциональный, строго типизированный, реактивный и управляемый событиями язык веб-клиента.

Вы можете начать задаваться вопросом, возможно ли программирование таким образом.

В действительности, эти качества в совокупности дают вам потрясающую парадигму программирования и разработки хорошего программного обеспечения.

Чисто функциональный

Вы можете подумать, что, используя более новую версию Java или ECMAScript 6, вы можете заниматься функциональным программированием. Но это только поверхность.

В этих языках программирования у вас все еще есть доступ к арсеналу языковых конструкций и искушение прибегнуть к его нефункциональным частям. Там, где вы действительно заметите разницу, это когда вы не можете делать ничего, кроме функционального программирования. Именно в этот момент вы начинаете чувствовать, насколько естественным может быть функциональное программирование.

В Elm почти все является функцией. Имя записи — это функция, значение типа объединения — это функция — каждая функция состоит из частично примененных функций к своим аргументам. Даже такие операторы, как плюс (+) и минус (-), являются функциями.

Для объявления языка программирования чисто функциональным, а не наличием таких конструкций, первостепенное значение имеет отсутствие всего остального. Только тогда вы сможете начать мыслить чисто функционально.

Elm создан по образцу зрелых концепций функционального программирования и напоминает другие функциональные языки, такие как Haskell и OCaml.

Строго типизированный

Если вы программируете на Java или TypeScript, то знаете, что это значит. Каждая переменная должна иметь ровно один тип.

Некоторые отличия, конечно, есть. Как и в случае с TypeScript, объявление типа является необязательным. Если нет, это будет выведено. Но нет «любого» типа.

Java поддерживает универсальные типы, но лучше. Обобщения в Java были добавлены позже, поэтому типы не являются универсальными, если не указано иное. И для их использования нам нужен уродливый синтаксис <> .

В Elm типы являются универсальными, если не указано иное. Давайте посмотрим на пример. Предположим, нам нужен метод, который берет список определенного типа и возвращает число. В Java это будет:

 public static <T> int numFromList(List<T> list){ return list.size(); }

И на языке вязов:

 numFromList list = List.length list

Хотя это и не обязательно, я настоятельно рекомендую всегда добавлять объявления типов. Компилятор Elm никогда бы не разрешил операции с неправильными типами. Человеку гораздо легче совершить такую ​​ошибку, особенно при изучении языка. Таким образом, приведенная выше программа с аннотациями типов будет выглядеть так:

 numFromList: List a -> Int numFromList list = List.length list

Поначалу может показаться непривычным объявлять типы в отдельной строке, но через некоторое время это начинает выглядеть естественно.

Язык веб-клиента

Это просто означает, что Elm компилируется в JavaScript, поэтому браузеры могут выполнять его на веб-странице.

Учитывая это, это не язык общего назначения, такой как Java или JavaScript с Node.js, а предметно-ориентированный язык для написания клиентской части веб-приложений. Более того, Elm включает в себя написание бизнес-логики (что делает JavaScript) и презентационной части (что делает HTML) — и все это на одном функциональном языке.

Все это делается с помощью очень специфического фреймворка, который называется The Elm Architecture.

реактивный

Elm Architecture — это реактивная веб-инфраструктура. Любые изменения в моделях немедленно отображаются на странице без явных манипуляций с DOM.

В этом он похож на Angular или React. Но Elm тоже делает это по-своему. Ключ к пониманию его основ находится в сигнатуре функций view и update :

 view : Model -> Html Msg update : Msg -> Model -> Model

Представление Elm — это не просто HTML-представление модели. Это HTML, который может создавать сообщения типа Msg , где Msg — это точный тип объединения, который вы определяете.

Любое стандартное событие страницы может создать сообщение. И когда создается сообщение, Elm внутренне вызывает функцию обновления с этим сообщением, которая затем обновляет модель на основе сообщения и текущей модели, и обновленная модель снова внутренне визуализируется в представлении.

Событийный

Как и JavaScript, Elm управляется событиями. Но в отличие, например, от Node.js, где для асинхронных действий предоставляются отдельные обратные вызовы, события Elm группируются в дискретные наборы сообщений, определенные в одном типе сообщений. И, как и в случае любого типа объединения, информация, которую несут значения отдельных типов, может быть любой.

Есть три источника событий, которые могут создавать сообщения: действия пользователя в представлении Html , выполнение команд и внешние события, на которые мы подписались. Вот почему все три типа, Html , Cmd и Sub , содержат msg в качестве аргумента. Кроме того, общий тип msg должен быть одинаковым во всех трех определениях — один и тот же, предоставленный функции обновления (в предыдущем примере это был тип Msg с заглавной M), где вся обработка сообщений централизована.

Исходный код реалистичного примера

Вы можете найти полный пример веб-приложения Elm в этом репозитории GitHub. Несмотря на простоту, он показывает большую часть функций, используемых в повседневном клиентском программировании: получение данных из конечной точки REST, декодирование и кодирование данных JSON, использование представлений, сообщений и других структур, взаимодействие с помощью JavaScript и все, что необходимо для компиляции и упаковки. Код Elm с Webpack.

Веб-пример ELM

Приложение отображает список пользователей, полученный с сервера.

Для упрощения процесса установки/демонстрации сервер разработки Webpack используется как для упаковки всего, включая Elm, так и для обслуживания пользовательского списка.

Часть функций реализована в Elm, часть — в JavaScript. Это сделано намеренно по одной важной причине: чтобы показать интероперабельность. Вероятно, вы захотите попробовать Elm для начала, либо постепенно переключить на него существующий код JavaScript, либо добавить новые функции в язык Elm. Благодаря совместимости ваше приложение продолжает работать как с кодом Elm, так и с кодом JavaScript. Это, вероятно, лучший подход, чем запускать все приложение с нуля в Elm.

Часть Elm в примере кода сначала инициализируется данными конфигурации из JavaScript, затем список пользователей извлекается и отображается на языке Elm. Допустим, у нас есть некоторые действия пользователя, уже реализованные в JavaScript, поэтому вызов действия пользователя в Elm просто отправляет ему обратный вызов.

В коде также используются некоторые концепции и методы, описанные в следующем разделе.

Применение концепций вяза

Давайте рассмотрим некоторые экзотические концепции языка программирования Elm в реальных сценариях.

Тип союза

Это чистое золото языка вязов. Помните все те ситуации, когда нужно было использовать разные по структуре данные с одним и тем же алгоритмом? Такие ситуации всегда сложно моделировать.

Вот пример: представьте, что вы создаете нумерацию страниц для своего списка. В конце каждой страницы должны быть ссылки на предыдущую, следующую и все страницы по их номерам. Как вы структурируете его, чтобы хранить информацию о том, по какой ссылке щелкнул пользователь?

Мы можем использовать несколько обратных вызовов для кликов предыдущего, следующего и номера страницы, или мы можем использовать одно или два логических поля, чтобы указать, что было нажато, или придать особое значение определенным целочисленным значениям, таким как отрицательные числа, ноль и т. д. Но ни один из эти решения могут моделировать именно такие пользовательские события.

В Элме это очень просто. Мы бы определили тип объединения:

 type NextPage = Prev | Next | ExactPage Int

И используем его как параметр для одного из сообщений:

 type Msg = ... | ChangePage NextPage

Наконец, мы обновляем функцию, чтобы иметь возможность проверять nextPage case

 update msg model = case msg of ChangePage nextPage -> case nextPage of Prev -> ... Next -> ... ExactPage newPage -> ...

Это делает вещи очень элегантными.

Создание нескольких функций карты с <|

Многие модули включают функцию map с несколькими вариантами для применения к различному количеству аргументов. Например, в List есть map , map2 , …, вплоть до map5 . Но что, если у нас есть функция, которая принимает шесть аргументов? map6 нет. Но есть техника, позволяющая это преодолеть. Он использует <| функция в качестве параметра и частичные функции с некоторыми аргументами, применяемыми в качестве промежуточных результатов.

Для простоты предположим, что в List есть только map и map2 , и мы хотим применить функцию, которая принимает три аргумента в трех списках.

Вот как выглядит реализация:

 map3 foo list1 list2 list3 = let partialResult = List.map2 foo list1 list2 in List.map2 (<|) partialResult list3

Предположим, мы хотим использовать foo , который просто умножает свои числовые аргументы, определенные следующим образом:

 foo abc = a * b * c

Таким образом, результат map3 foo [1,2,3,4,5] [1,2,3,4,5] [1,2,3,4,5] равен [1,8,27,64,125] : List number .

Давайте разберем, что здесь происходит.

Во-первых, в partialResult = List.map2 foo list1 list2 , foo частично применяется к каждой паре в list1 и list2 . Результатом является [foo 1 1, foo 2 2, foo 3 3, foo 4 4, foo 5 5] , список функций, которые принимают один параметр (поскольку первые два уже применены) и возвращают число.

Далее в List.map2 (<|) partialResult list3 на самом деле это List.map2 (<|) [foo 1 1, foo 2 2, foo 3 3, foo 4 4, foo 5 5] list3 . Для каждой пары этих двух списков мы вызываем функцию (<|) . Например, для первой пары это (<|) (foo 1 1) 1 , что совпадает с foo 1 1 <| 1 foo 1 1 <| 1 , который совпадает с foo 1 1 1 , который производит 1 . Для второго это будет (<|) (foo 2 2) 2 , то есть foo 2 2 2 , что равно 8 и так далее.

Этот метод может быть особенно полезен с функциями mapN для декодирования объектов JSON со многими полями, поскольку Json.Decode предоставляет их вплоть до map8 .

Удалить все пустые значения из списка возможных вариантов

Допустим, у нас есть список значений « Maybe », и мы хотим извлечь только значения из тех элементов, у которых оно есть. Например, список такой:

 list : List (Maybe Int) list = [ Just 1, Nothing, Just 3, Nothing, Nothing, Just 6, Just 7 ]

И мы хотим получить [1,3,6,7] : List Int . Решение - это однострочное выражение:

 List.filterMap identity list

Давайте посмотрим, почему это работает.

List.filterMap ожидает, что первым аргументом будет функция (a -> Maybe b) , которая применяется к элементам предоставленного списка (второй аргумент), и результирующий список фильтруется, чтобы опустить все значения Nothing , а затем реальный значения извлекаются из Maybe s.

В нашем случае мы предоставили identity , поэтому результирующий список снова будет [ Just 1, Nothing, Just 3, Nothing, Nothing, Just 6, Just 7 ] . После фильтрации получаем [ Just 1, Just 3, Just 6, Just 7 ] , а после извлечения значения — [1,3,6,7] , как мы и хотели.

Пользовательское декодирование JSON

Поскольку наши потребности в декодировании (или десериализации) JSON начинают превышать то, что представлено в модуле Json.Decode , у нас могут возникнуть проблемы с созданием новых экзотических декодеров. Это связано с тем, что эти декодеры вызываются в середине процесса декодирования, например, внутри методов Http , и не всегда ясно, каковы их входные и выходные данные, особенно если в предоставленном JSON много полей.

Вот два примера, чтобы показать, как обрабатывать такие случаи.

В первом у нас есть два поля во входящем JSON, a и b , обозначающие стороны прямоугольной области. Но в объекте Elm мы хотим сохранить только его площадь.

 import Json.Decode exposing (..) areaDecoder = map2 (*) (field "a" int) (field "b" int) result = decodeString areaDecoder """{ "a":7,"b":4 }""" -- Ok 28 : Result.Result String Int

Поля декодируются по отдельности с помощью декодера field int , а затем оба значения передаются предоставленной функции в map2 . Поскольку умножение ( * ) также является функцией и принимает два параметра, мы можем просто использовать ее так. Результирующий areaDecoder является декодером, который возвращает результат применения функции, в данном случае a*b .

Во втором примере мы получаем беспорядочное поле состояния, которое может быть нулевым или любой строкой, включая пустую, но мы знаем, что операция прошла успешно, только если она «ОК». В этом случае мы хотим сохранить его как True , а во всех остальных случаях — как False . Наш декодер выглядит так:

 okDecoder = nullable string |> andThen (\ms -> case ms of Nothing -> succeed False Just s -> if s == "OK" then succeed True else succeed False )

Давайте применим его к некоторым JSON:

 decodeString (field "status" okDecoder) """{ "a":7, "status":"OK" }""" -- Ok True decodeString (field "status" okDecoder) """{ "a":7, "status":"NOK" }""" -- Ok False decodeString (field "status" okDecoder) """{ "a":7, "status":null }""" -- Ok False

Ключ здесь в функции, предоставленной andThen , которая берет результат предыдущего декодера строки, допускающей значение NULL (который является Maybe String ), преобразует его в то, что нам нужно, и возвращает результат как декодер с помощью succeed .

Ключевые вынос

Как видно из этих примеров, функциональное программирование может быть не очень интуитивно понятным для разработчиков Java и JavaScript. Требуется некоторое время, чтобы привыкнуть к этому, с большим количеством проб и ошибок. Чтобы лучше понять это, вы можете использовать elm-repl для тренировки и проверки возвращаемых типов ваших выражений.

Пример проекта, указанный ранее в этой статье, содержит еще несколько примеров пользовательских декодеров и кодировщиков, которые также могут помочь вам понять их.

Но все же, почему выбирают вяз?

Будучи настолько отличным от других клиентских фреймворков, язык Elm, конечно, не является «еще одной библиотекой JavaScript». Таким образом, у него есть много черт, которые можно считать положительными или отрицательными по сравнению с ними.

Начнем с положительной стороны.

Клиентское программирование без HTML и JavaScript

Наконец, у вас есть язык, на котором вы можете делать все это. Больше никаких разлук и несуразных комбинаций их смешения. Никакой генерации HTML в JavaScript и никаких пользовательских языков шаблонов с некоторыми урезанными логическими правилами.

С Elm у вас есть только один синтаксис и один язык во всей его красе.

Единообразие

Поскольку почти все концепции основаны на функциях и некоторых структурах, синтаксис очень лаконичен. Вам не нужно беспокоиться, если какой-то метод определен на уровне экземпляра или класса, или если это просто функция. Все они являются функциями, определенными на уровне модуля. И нет сотни разных способов перебора списков.

В большинстве языков всегда существует спор о том, написан ли код в соответствии с языком. Многие идиомы должны быть освоены.

В Elm, если он компилируется, это, вероятно, способ "Elm". Если нет, то уж точно нет.

Выразительность

Несмотря на краткость, синтаксис Elm очень выразителен.

Это достигается в основном за счет использования типов объединения, формальных объявлений типов и функционального стиля. Все это вдохновляет на использование более мелких функций. В конце вы получаете код, который в значительной степени самодокументируется.

Нет нуля

Когда вы используете Java или JavaScript в течение очень долгого времени, null становится для вас чем-то совершенно естественным — неизбежной частью программирования. И, хотя мы постоянно видим NullPointerException и различные TypeError , мы все равно не думаем, что реальная проблема заключается в существовании null . Это так естественно .

Это становится ясно быстро через некоторое время с Elm. Отсутствие null не только освобождает нас от необходимости снова и снова видеть ошибки ссылок на нуль во время выполнения, но также помогает нам писать более качественный код, четко определяя и обрабатывая все ситуации, когда у нас может не быть фактического значения, тем самым также уменьшая технический долг, не откладывая null . управляемость, пока что-нибудь не сломается.

Уверенность в том, что это сработает

Создать синтаксически корректные программы на JavaScript можно очень быстро. Но будет ли это работать на самом деле? Что ж, давайте посмотрим после перезагрузки страницы и тщательного тестирования.

С Эльмом все наоборот. Со статической проверкой типов и принудительной null нулей требуется некоторое время для компиляции, особенно когда программу пишет новичок. Но, как только он скомпилируется, есть большая вероятность, что он будет работать правильно.

Быстро

Это может быть важным фактором при выборе клиентского фреймворка. Скорость отклика обширного веб-приложения часто имеет решающее значение для взаимодействия с пользователем и, следовательно, для успеха всего продукта. И, как показывают тесты, Elm очень быстр.

Плюсы Elm по сравнению с традиционными фреймворками

Большинство традиционных веб-фреймворков предлагают мощные инструменты для создания веб-приложений. Но за эту мощь приходится платить: чрезмерно сложная архитектура с множеством различных концепций и правил о том, как и когда их использовать. Нужно много времени, чтобы все это освоить. Есть контроллеры, компоненты и директивы. Затем идут этапы компиляции и настройки, а также этап запуска. Кроме того, есть сервисы, фабрики и весь пользовательский язык шаблонов, используемый в предоставленных директивах — все те ситуации, когда нам нужно вызвать $scope.$apply() напрямую, чтобы обновить страницу, и многое другое.

Компиляция Elm в JavaScript, безусловно, также очень сложна, но разработчик защищен от необходимости знать все его тонкости. Просто напишите Elm и позвольте компилятору сделать свою работу.

И почему бы не выбрать вяз?

Хватит восхвалять Вяз. Теперь давайте посмотрим на его не очень хорошую сторону.

Документация

Это действительно серьезная проблема. В языке Elm отсутствует подробное руководство.

Официальные туториалы просто просматривают язык и оставляют много вопросов без ответа.

Официальная ссылка на API еще хуже. Многим функциям не хватает каких-либо объяснений или примеров. И есть те, у кого есть предложение: «Если это сбивает с толку, поработайте с Учебником по архитектуре Elm. Это действительно помогает!» Не лучшая строка, которую вы хотели бы видеть в официальной документации API.

Надеюсь, это скоро изменится.

Я не верю, что Elm может получить широкое распространение с такой плохой документацией, особенно среди людей, пришедших с Java или JavaScript, где такие концепции и функции вообще не интуитивно понятны. Чтобы понять их, требуется гораздо лучшая документация с большим количеством примеров.

Формат и пробелы

Избавление от фигурных скобок или круглых скобок и использование пробелов для отступов может выглядеть неплохо. Например, код Python выглядит очень аккуратно. Но создателям elm-format этого было мало.

Со всеми двойными пробелами между строками, а также выражениями и присваиваниями, разделенными на несколько строк, код Elm выглядит более вертикальным, чем горизонтальным. То, что было бы однострочником в старом добром C, может легко растянуться на несколько экранов в языке Elm.

Это может звучать хорошо, если вам платят за написанные строки кода. Но если вы хотите выровнять что-то с выражением, начатым на 150 строк раньше, удачи в поиске правильного отступа.

Обработка записей

Работать с ними сложно. Синтаксис для изменения поля записи уродлив. Нет простого способа изменить вложенные поля или произвольно ссылаться на поля по имени. И если вы используете функции доступа общим способом, возникает много проблем с правильной типизацией.

В JavaScript запись или объект — это центральная структура, которую можно создать, получить к ней доступ и изменить множеством способов. Даже JSON — это просто сериализованная версия записи. Разработчики привыкли работать с записями в веб-программировании, поэтому трудности с их обработкой в ​​Elm могут стать заметными, если они используются в качестве основной структуры данных.

Больше ввода

Elm требует написания большего количества кода, чем JavaScript.

Неявное преобразование типов для строковых и числовых операций отсутствует, поэтому требуется много преобразований int-float и особенно вызовов toString , которые затем требуют, чтобы круглые скобки или символы приложения функции соответствовали правильному количеству аргументов. Кроме того, функция Html.text требует в качестве аргумента строку. Требуется много case-выражений для всех этих Maybe s, Results , типов и т.д.

Основной причиной этого является строгая система типов, и это может быть справедливой ценой.

Декодеры и кодировщики JSON

Одной из областей, где действительно выделяется больше ввода, является обработка JSON. То, что является простым JSON.parse() в JavaScript, может занимать сотни строк в языке Elm.

Конечно, необходимо какое-то сопоставление между структурами JSON и Elm. Но необходимость писать и декодеры, и энкодеры для одного и того же куска JSON — серьезная проблема. Если ваши REST API передают объекты с сотнями полей, это потребует много работы.

Заворачивать

Мы увидели Elm, пришло время ответить на известные вопросы, возможно, такие же старые, как и сами языки программирования: лучше ли он конкурентов? Должны ли мы использовать его в нашем проекте?

Ответ на первый вопрос может быть субъективным, так как не каждый инструмент является молотком и не все является гвоздем. Elm может блистать и быть лучшим выбором по сравнению с другими фреймворками веб-клиентов во многих случаях, но проигрывать в других. Но он предлагает действительно уникальную ценность, которая может сделать разработку веб-интерфейса намного безопаснее и проще, чем альтернативы.

На второй вопрос, чтобы не отвечать старым «это зависит», ответ прост: да. Даже со всеми упомянутыми недостатками, просто уверенность, которую Elm дает вам в правильности вашей программы, является достаточной причиной для его использования.

Программировать в Elm тоже весело. Это совершенно новая перспектива для тех, кто привык к «обычным» парадигмам веб-программирования.

В реальном использовании вам не нужно сразу переключать все приложение на Elm или полностью запускать в нем новое. Вы можете использовать его совместимость с JavaScript, чтобы попробовать, начиная с части интерфейса или некоторых функций, написанных на языке Elm. Вы быстро узнаете, соответствует ли он вашим потребностям, а затем расширите его использование или откажетесь от него. И кто знает, возможно, вы тоже влюбитесь в мир функционального веб-программирования.

Связанный: Раскопки ClojureScript для фронтенд-разработки