Что если написать код на D в духе Elm?
Я люблю эксперименты, особенно когда это касается языков программирования. И вот, однажды мне пришла в голову переписать какой-нибудь пример с Elm на D. Но перед тем как собственно рассказывать, что из этого получилось, давайте разберёмся, что это за фрукты и с чем их едят.
Elm
Elm — это язык с функциональным программированием для веб приложений в духе Haskell. Если вы забыли (или не знали) функциональное программирование представляет программу как функцию близкую к математическому смыслу, которая будет вычисляться. Например Фибоначчи будет вычисляться так:
fib n = case n of 1 -> 1 2 -> 2 _ -> (fib n - 1) + (fib n - 2)
В то время как для более нормальных языков вы будете указывать их скорее всего как набор шагов:
function fib(n) { let fib1 = 1; let fib2 = 1; if(n == 1 || n == 2) { return 1; } for(let i = 2; i <= n; i++) { let tmp = fib2; fib2 = fib1 + fib2; fib1 = tmp; } return fib2; }
Понятное дело, пример немного высосан из пальца: из-за того что большинство современных языков поддерживают не одну парадигму, а сразу же несколько, на вашем любимом языке вы можете писать код и так и этак. Ну, скорее всего...
С другой стороны языки полностью погруженные в функциональное программирование, не могут в остальное. Плата за это — надёжность кода и отлавливание ошибок на уровне компиляции. В этом и смысл Elm — вы найдёте баги просто скомпилировав программу.
Но как же TypeScript (или ReScript/ReasonML)? В отличии от Elm они привязаны непосредственно к JS, таким образом предоставляя в принципе меньшие гарантии. Не то что бы это плохо, но это довольно существенное отличие, о котором надо помнить.
D
D aka Dlang — внебрачный сын С++ и Java. По задумке он более простой и одновременно мощный язык чем С++, но в своё время не взлетел (по мнению плюсовиков из-за наличия GC) и теперь где-то пылится. Впрочем, язык работоспособен и вполне сгодится если вы хотите менее популярную и более простую альтернативу С++.
Принципиальных отличий для тех кто видел С-подобные языки (С++, С#, Java...) будет маловато, но хочется обратить внимание на такой момент:
В традиционных языках шаблоны/дженерики пишутся через <…>:
template foo<T> {...} foo<int>
В D же они чаще всего определяются через круглые скобочки и используются через восклицательный знак:
template foo(T) {...} foo!(int) foo!int // так можно если параметр всего один
А с остальным мы уже по ходу дела разберёмся.
Суть
Как вы наверняка догадываетесь, традиционные способы сделать приложение как например, в React тут не работают. С другой стороны Model-View-Controller внезапно, это не особо мешает. Кому интересно вникнуть — можно почитать гайд на сайте Elm, там всё просто и понятно даже для маленьких || тупых.
Для портирования я взял простенький пример с кнопками который можно потыкать интерактивно и целиком тут (восхититься и ужаснуться). Ну, а мы его будем разбирать по частям.
Сразу же хочется оговориться: на D приложение естественно не работает, но компилируется, а значит можно доделать до рабочего состояния.
Модули
В Elm есть модули. Мы их видим буквально же первой строчкой:
import Browser import Html exposing (Html, button, div, text) import Html.Events exposing (onClick)
В D тоже есть модули. Никаких проблем? Увы, есть. Дьявол в деталях модуль Html экспортирует символ Html, который мы добавляем в глобальное пространство имён чтобы не писать каждый раз Html.Html, а просто Html. Но в D у нас происходит конфликт имён: как понять где мы имеем в виду модуль Html, а где тип Html? Никак, переименовывайте модуль или не экспортируйте в глобальное пространство имён.
Я решил пойти путём наименьшего сопротивления и переименовал модуль в Html_. Получилось так:
import Browser; import Html_ : Html, button, div, text; import Html_.Events : onClick;
Вариант является почти полным аналогом. Неплохо, но впоследствии нам потребуется ещё парочка импортов поэтому пишем:
import std.sumtype; import std.conv : to;
Main
Сердце кода — это функция с которой всё начинается. В Elm всё прекрасно и красиво:
main = Browser.sandbox { init = init, update = update, view = view }
Я не буду вдаваться в детали, но замечу, что в фигурных скобочках объявляется структура. Ну знаете, как словарь (ключ -> значение), только статический. И тут возникает проблема — анонимных структур в D нет. То есть нам надо где-то её объявить, присвоив имя, а потом её создать.
Погодите, но мы не знаем типов init, update и view заранее, а значит структуру надо делать шаблонной. А автовывод сложных шаблонных типов, почему-то не работает — подозреваю что такие конструкции его не учили прожевывать. И вот, шаг за шагом у нас получается чудовище (Франкенштейн одобряет):
void main() { Browser.sandbox!(Model, Msg)(Sandbox!(Model, Html!Msg function(Model), Model function(Msg, Model))(init, &view, &update)); }
Ладно, мы можем немного пойти в обход и вместо структуры сделать просто функцию:
void main() { Browser.sandbox!(Model, Msg)(init, &view, &update); }
Код стал читаемее, но автовывод типов все ещё по нам плачет. Да и уже не 1 в 1.
Model
Модель в MVC это данные. А данные у нас в примере максимально простые — один Int. Для того чтобы модель была расширяемая, мы добавляем псевдоним (alias) типа и определяем изначальное значение:
type alias Model = Int init : Model init = 0
Тут у нас уже никаких проблем нет и код получается плюс минус идентичный:
alias Model = int; Model init = 0;
Update
Вот тут начинаются причуды архитектуры. Функция update принимает все события происходящие в приложения и на них реагирует. На практике это не очень страшно (мы можем маршрутизировать события сами), но это довольно сильно отличается от традиционного React и иже с ним. Код такой:
type Msg = Increment | Decrement update : Msg -> Model -> Model update msg model = case msg of Increment -> model + 1 Decrement -> model - 1
Сначала поясним что за Msg странный такой. Это тип-сумма олицетворяющая тип событий. Немного упрощая, нам могут приходить события либо вида Increment, либо Decrement — а все другие приходить не могут. Совсем никак не могут.
С чтением case у вас не должно возникнуть сложностей — это обычный switch-case только с невидимым return. Аналогично, с описанием типа функции тоже не должно быть проблем.
Переписывать код на D будет немного сложно. Дело в том что Increment и Decrement это конструкторы типа Msg. То есть у нас возникает некоторое раздвоение личности, которое многим более консервативным языкам не нравится и они хотят интерпретировать Increment и Decrement как самостоятельные типы. А Msg — как их объединение. Тут ничего не попишешь и придётся играть по их правилам. Помните import std.sumtype? Вот он нам сейчас и пригодится:
struct Increment {} struct Decrement {} alias Msg = SumType!(Increment, Decrement); Model update(Msg msg, Model model) { return msg.match!( (Increment _) => model + 1, (Decrement _) => model - 1 ); }
Отдельно хочется упомянуть match. На самом деле я наврал и case-of является не switch-case, он имеет больше скрытой мощи. Он имеет нечто, что называют pattern matching. Функция match в D создана чтобы достичь этой мощи, но более неудобным путём.
View
Модель и Контроллер у нас есть, остаётся Представление. Тут всё просто, собираем DOM на пальцах:
view : Model -> Html Msg view model = div [] [ button [ onClick Decrement ] [ text "-" ] , div [] [ text (String.fromInt model) ] , button [ onClick Increment ] [ text "+" ] ]
Функции вызываются без скобочек и через пробел — но стоит лишь привыкнуть и вы с лёгкостью прочитаете код.
Ещё один момент, а именно Html Msg. Это дженерик (или шаблон) и Msg подставляется прямо в тип, поэтому на D его надо переписать в Html!Msg:
Html!Msg view(Model model) { return div([], [ button([ onClick(Decrement()) ], [ text("-") ]), div([], [ text(model.to!string) ]), button([ onClick(Increment()) ], [ text("+") ]) ]); }
Итог
Писать на D словно ты пишешь на Elm можно. Я не пробовал воспроизвести логику (хотя бы потому что D не компилируется в JS), но выглядит вполне возможным написание и всей остальной логики.
Из минусов — нет анонимных структур и автовывода шаблонных типов. Но жить можно (не обязательно хорошо).