LEARN X · ЗА 16 МИН

Elm

Экспресс-тур по Elm: чисто функциональный язык для веба без runtime-ошибок. Типы, списки, записи, Maybe, архитектура Model-Update-View.

Elm — чисто функциональный язык для веб-фронтенда, который компилируется в JavaScript и славится отсутствием runtime-ошибок. Строгая статическая типизация, иммутабельность и архитектура Model-Update-View. Весь язык — на одной странице через комментарии в коде.

Комментарии и вывод

-- Однострочный комментарий начинается с двух дефисов.

{- Многострочный комментарий
   может занимать несколько строк.
   {- И даже влагаться внутрь другого. -}
-}

-- Базовые значения и их литералы:
42              -- целое число (Int)
3.14            -- число с плавающей точкой (Float)
True            -- логическое (Bool): True или False
'a'             -- символ (Char) — в одинарных кавычках
"Привет"        -- строка (String) — в двойных

-- Арифметика. Деление: / для Float, // для целочисленного.
2 + 3 * 4       -- 14
10 / 3          -- 3.3333 (Float)
10 // 3         -- 3 (целочисленное деление)
2 ^ 10          -- 1024 (возведение в степень)

-- Конкатенация строк оператором ++
"Code" ++ "chick"   -- "Codechick"

Функции

Функции — основа языка. Аргументы пишутся через пробел, без скобок.

-- Определение функции: имя аргументы = тело
add x y =
    x + y

-- Применение (аппликация): аргументы через пробел
add 2 3          -- 5

-- Лямбда (анонимная функция): \аргументы -> тело
double =
    \n -> n * 2

double 21        -- 42

-- Частичное применение: все функции каррированы.
-- Передаём меньше аргументов — получаем новую функцию.
addTen =
    add 10       -- функция, ждущая один аргумент

addTen 5         -- 15

-- Оператор |> (pipe): передаёт значение слева как
-- последний аргумент функции справа. Читается слева направо.
5
    |> add 10     -- 15
    |> double    -- 30

Типы и аннотации

Elm строго типизирован. Типы выводятся автоматически, но аннотации принято писать явно.

-- Аннотация типа идёт над определением, через ::
-- Стрелка -> разделяет аргументы и результат.
add : Int -> Int -> Int
add x y =
    x + y

-- Базовые типы:
age : Int          -- целое число
age = 25

pi : Float         -- число с точкой
pi = 3.14159

isReady : Bool     -- логическое
isReady = True

name : String      -- строка
name = "Elm"

grade : Char       -- символ
grade = 'A'

-- Важно: Int и Float не смешиваются неявно.
-- toFloat и round/floor преобразуют явно.
toFloat 10 / 3     -- 3.3333

Списки

Список (List) — упорядоченная коллекция элементов одного типа.

numbers : List Int
numbers = [ 1, 2, 3, 4, 5 ]

empty = []                  -- пустой список

-- Добавление в начало оператором :: (cons)
0 :: numbers                -- [0, 1, 2, 3, 4, 5]

-- Объединение списков через ++
[ 1, 2 ] ++ [ 3, 4 ]        -- [1, 2, 3, 4]

-- map: применить функцию к каждому элементу
List.map (\n -> n * 2) numbers   -- [2, 4, 6, 8, 10]

-- filter: оставить элементы, удовлетворяющие условию
List.filter (\n -> n > 2) numbers  -- [3, 4, 5]

-- foldl: свёртка слева направо (аккумулятор)
List.foldl (+) 0 numbers         -- 15 (сумма)

-- Другие полезные функции:
List.length numbers              -- 5
List.reverse numbers             -- [5, 4, 3, 2, 1]
List.sum numbers                 -- 15

Кортежи и записи

Кортеж (tuple) группирует фиксированное число значений разных типов. Запись (record) — именованные поля.

-- Кортеж: значения в круглых скобках через запятую
point : ( Int, Int )
point = ( 3, 4 )

-- Извлечение для пар — Tuple.first / Tuple.second
Tuple.first point      -- 3
Tuple.second point     -- 4

-- Запись: поля с именами в фигурных скобках
user : { name : String, age : Int }
user =
    { name = "Alice", age = 30 }

-- Доступ к полю через точку
user.name              -- "Alice"

-- Иммутабельное обновление: { запись | поле = новое }
-- Создаёт НОВУЮ запись, старая не меняется.
olderUser =
    { user | age = 31 }

olderUser.age          -- 31
user.age               -- 30 (осталась прежней)

Условия

-- if/then/else — всегда выражение, ветка else обязательна.
-- Обе ветки должны возвращать значение одного типа.
describe : Int -> String
describe n =
    if n > 0 then
        "положительное"
    else if n < 0 then
        "отрицательное"
    else
        "ноль"

-- case/of — сопоставление с образцом. Мощнее if.
russian : Int -> String
russian n =
    case n of
        1 -> "один"
        2 -> "два"
        3 -> "три"
        _ -> "много"   -- _ ловит всё остальное

Пользовательские типы

Ключевое слово type создаёт свои типы с несколькими вариантами (union types).

-- Перечисление вариантов через |
type Color
    = Red
    | Green
    | Blue

-- Варианты могут нести данные:
type Shape
    = Circle Float            -- радиус
    | Rectangle Float Float   -- ширина и высота

-- Maybe — встроенный тип для "может быть пусто".
-- Заменяет null: type Maybe a = Just a | Nothing
findAge : String -> Maybe Int
findAge name =
    if name == "Alice" then
        Just 30
    else
        Nothing

-- Result — для операций, которые могут упасть.
-- type Result error value = Ok value | Err error
safeDivide : Float -> Float -> Result String Float
safeDivide a b =
    if b == 0 then
        Err "деление на ноль"
    else
        Ok (a / b)

Сопоставление с образцом

case разбирает пользовательские типы. Компилятор требует обработать ВСЕ варианты.

-- Разбор union-типа с извлечением данных
area : Shape -> Float
area shape =
    case shape of
        Circle r ->
            3.14159 * r * r

        Rectangle w h ->
            w * h

-- Разбор Maybe — безопасная работа с отсутствием
greet : Maybe Int -> String
greet maybeAge =
    case maybeAge of
        Just age ->
            "Возраст: " ++ String.fromInt age

        Nothing ->
            "Возраст неизвестен"

-- Разбор списка по голове и хвосту (x :: rest)
firstOrZero : List Int -> Int
firstOrZero list =
    case list of
        [] ->
            0

        x :: _ ->
            x

Функции высшего порядка и композиция

Функции можно передавать в другие функции и склеивать в конвейеры.

-- Функция принимает другую функцию аргументом
applyTwice : (a -> a) -> a -> a
applyTwice f x =
    f (f x)

applyTwice (\n -> n + 3) 0   -- 6

-- Композиция слева направо: >>
-- (f >> g) x равно g (f x)
incThenDouble =
    (\n -> n + 1) >> (\n -> n * 2)

incThenDouble 5             -- 12

-- Композиция справа налево: <<
-- (f << g) x равно f (g x)
doubleThenInc =
    (\n -> n + 1) << (\n -> n * 2)

doubleThenInc 5             -- 11

Модули

-- Объявление модуля и что он экспортирует.
-- exposing (..) открывает всё; лучше перечислять явно.
module Math exposing (add, square)

-- Импорт модуля целиком (доступ через List.map)
import List

-- Импорт с алиасом (Html.Attributes → Attr)
import Html.Attributes as Attr

-- Импорт конкретных имён в текущее пространство
import Html exposing (div, text)

-- Импорт типа со всеми его вариантами
import Maybe exposing (Maybe(..))

Архитектура Elm (Model-Update-View)

Любое Elm-приложение строится на трёх частях: состояние (Model), обновление (update) и отображение (view). Это и есть "The Elm Architecture".

-- 1. MODEL — всё состояние приложения
type alias Model =
    { count : Int }

init : Model
init =
    { count = 0 }

-- 2. MESSAGES — что может произойти
type Msg
    = Increment
    | Decrement

-- 3. UPDATE — как меняется Model в ответ на Msg
-- Возвращает НОВЫЙ Model (иммутабельно).
update : Msg -> Model -> Model
update msg model =
    case msg of
        Increment ->
            { model | count = model.count + 1 }

        Decrement ->
            { model | count = model.count - 1 }

-- 4. VIEW — как Model превращается в HTML.
-- Клики порождают Msg, которые идут в update.
view : Model -> Html Msg
view model =
    div []
        [ button [ onClick Decrement ] [ text "-" ]
        , text (String.fromInt model.count)
        , button [ onClick Increment ] [ text "+" ]
        ]

Особенности Elm

-- НЕТ null И undefined.
-- Отсутствие значения выражается типом Maybe,
-- поэтому "невозможно прочитать свойство null" не бывает.
safe : Maybe Int
safe = Nothing

-- НЕТ исключений (exceptions) во время выполнения.
-- Ошибки выражаются типом Result и обрабатываются явно.
parse : String -> Result String Int
parse s =
    String.toInt s
        |> Result.fromMaybe "не число"

-- ИММУТАБЕЛЬНОСТЬ: данные нельзя изменить на месте.
-- Любое "изменение" создаёт новое значение.
original = [ 1, 2, 3 ]
changed = 0 :: original    -- [0, 1, 2, 3]
-- original всё ещё [1, 2, 3]

-- ЧИСТОТА: функции без побочных эффектов.
-- Один вход — всегда один выход. Все эффекты
-- (HTTP, время) управляются через Cmd и Sub.
Поддержать проект