Дорогие читатели‑питонисты, хочу вас поздравить с выходом питона 3.15! А пишущим на эрланге хочу напомнить, что OTP 30 уже наполовину готова.
Этот пост — продолжение саги, начатой предыдущим поросячьим постом. Моя идея насчёт нового языка для BEAM не только не исчезла, но и получила развитие. Правда, брендинг теперь другой, все поросята были выгнаны. Теперь на обложке угорь — Eel.
Как это обычно бывает, после написания предыдущего поста я узнал, что Erlang в чистом виде — изящный и минималистичный язык. Но это не отбило у меня охоту экспериментировать. В результате, получилось нечто, сильно навеянное эрлангом.
Эрлангисты должны оценить: переменные в Eel иммутабельны — как в эрланге. То есть, нельзя писать X = X + 1, а нужно писать NewX = X + 1.
Вообще, основную мысль статьи хорошо передаёт сегодняшняя новость в газете Фонтанка. Вот она:

Я думаю, в конце статьи вы со мной согласитесь — кабанов обязательно нужно выпроваживать, особенно загостившихся.
Итак, я начал с того, что эрланг совсем не такой странный, каким кажется. И, когда я это осознал, то многие «странности» из эрланга перенёс в Eel как они есть.
Атомы и переменные
Одна из вещей, которая бросается в глаза в эрланге — это атомы, которые ничем не отделяются. Это просто голые идентификаторы с маленькой буквы, которые стоят рядом с вызовами функций. Путаницы не возникает: если после идентификатора стоят стобки, значит, это вызов функции. На самом деле, названия модулей и функций — это тоже атомы.
Если что, в примерах с кодом я буду рекламировать свой синтаксис — он плюс‑минус совпадает с синтаксисом питона.
ok, Value = my_module.my_func()
ok — атом, Value — переменная.
Переменные — это второе, что бросается в глаза в эрланге, они начинаются с большой буквы. Опять же, сделано это специально: это локальный контекст, который должен выделяться на фоне внешнего контекста.
В Eel, переменные тоже начинаются с большой буквы. По код‑стайлу эрланга, для атомов и названий используется snake_case, для переменных — CamelCase.
Импорты и экспорты
Выглядят они в эрланге так:
%% -export([имя_функции/арность]). -export([list_even/2]). %% -import(имя_модуля, [имя_функции/арность]). -import(lists, [map/2, seq/2]). list_even(N) -> %% Благодаря импорту выше, мы пишем seq(X, Y) вместо lists:seq(X, Y) Numbers = seq(1, N), map(fun(N) -> N * 2 end, Numbers).
Импорт позволяет в коде обращаться к функции по имени, не указывая, из какого модуля она берётся. Можно обойтись и без этого:
def list_even(N): Numbers = lists.seq(1, N) map(lambda Num: Num * 2, Numbers)
На самом деле, код‑стайл эрланга так и рекомендует: по возможности, не использовать импорты, а указывать полное имя функции, вместе с модулем. В Eel импорты транслируются очень просто:
from lists import map, seq
Арность функций не указывается, пусть импортируются все функции с нужным именем. Многоуровневых импортов, как в питоне, нет:
from httpx._models.requests import Request # такое не поддерживается!
Экспорты в эрланге нужны для приватной/публичной видимости. Я, как программист на питоне отлично обхожусь без приватных функций, но, может быть, кому‑то они нужны. В Eel будет возможность определить приватную функцию словом defp.
Иммутабильность переменных
Наконец, мы подошли к самому интересному: переменные не могут менять своё значение. То есть, что нельзя написать X = X + 1 . Я считаю, что неизменяемые переменные отлично подходят для рантайма, где неизменяемы сами данные, и что последний факт не нужно каким‑то образом прятать.
В Eel, для всех новых значений придётся заводить новую переменную. Так что, готовьтесь придумывать префиксы и суффиксы для своих переменных:
ActivatedUser = BaseUser | {status: active} PublicUser = maps.with([name, status], ActivatedUser)
Кортежи, списки и словари
В эрланге:
% Кортеж User = {user, 1, "Alex", 25}, % Список Numbers = [1, 2, 3, 4, 5], % Разделение на голову и хвост [Head | Tail] = Numbers % Словарь Book = #{ title => "Erlang Programming", author => "Joe Armstrong", year => 2009 },
В питоне:
# Кортеж User = (user, 1, "Alex", 25) # Список Numbers = [1, 2, 3, 4, 5] # Разделение на голову и хвост [Head, *Tail] = Numbers # Словарь Book = { title: "Erlang Programming", author: "Joe Armstrong", year: 2009 }
Как видите, совпадает у них только обозначение списка. В эрланге, {1, 2} — это кортеж, а в питоне — множество.
Так вот, в вопросе выбора синтаксиса для литералов мне захотелось проявить демократичность. Как минимум — чтобы можно было вставлять конфиги из документации эрланга через Copy‑Paste.
Я склоняюсь к тому, чтобы разрешить использовать синтаксис {1, 2} для кортежей наравне с (1, 2) и синтаксис #{K => V} для словарей. Аналогично, разрешить синтаксис
Map = #{x => 0} UpdatedMap = Map#{y => 1} # создание нового ключа или обновление старого NewMap = UpdatedMap#{x := 1} # только обновление старого ключа
В конце концов, сочетание символов => в питоне не используется, как и #{..}. Так что, парсинг усложнится не сильно.
List/dict comprehensions
Циклы for в языках, где данные неизменяемы, не нужны. Зато, нужны list/dict comprehensions. Синтаксис для них я взял из питона
NewMap = { K: 2 * V # Можно использовать =>, если очень хочется for K, V in Map }
В питоне итерация по словарю дает нам ключи, но в эрланге принято отдавать пары ключ‑значение. Аналогично, будет и tuple/record comprehension:
EvenNumbers = ( # можно использовать и {...} 2 * N for N in seq(1, 10) )
Генераторы (ленивые списки, streams) в каком‑то виде в Eel, вероятно, будут, но использовать для них синтаксис (...), как в питоне, я не хочу. Например, в примере ниже — кортеж, а не генератор:
Numbers = (X for X in seq(1, 10)) # кортеж, а не генератор, как в питоне
Опциональная статическая типизация
Киллер‑фича и новинка сезона!
Уж не знаю, что такого в этой статической типизации, но она вдруг всем стала нужна. И в Eel, посколько синтаксис для функций взят из питона, их параметры можно аннотировать типами. И им ничего не мешает превращаться в эрланговские guards:
def f(x: int, y: int): # превращается в when is_integer(X), is_integer(Y)
Для использования паттерн‑матчинга при объявлении функций я придумал конструкцию match def. Подробно останавливаться на ней я не буду, но так, например, выглядит функция вычисления чисел Фибоначчи:
def fib(N): fib(N, 0, 1) match def fib(N, Current, Next): case N = 0: # это паттерн-матчинг Current case if N > 0: # в питоновском match..case условия пишутся после if fib(N - 1, Next, Current + Next)
Как видите, всё‑таки, match def не такой гибкий, как эрланговский паттерн‑матчинг: он матчит только функции с одинаковым числом аргументов. Но, думаю — и такой сойдёт.
Пайплайны
Ни в питоне, ни в эрланге пайплайнов нет, но они очень идут функциональному языку. И я думаю, неплохо бы их добавить:
EvenNumbers = seq(1, 10) ..map(lambda N: N * 2) EvenNumbers = seq(1, 10) ..map: lambda N: N * 2
Это 2 эквивалентных способа написать одно и то же.
На этом, я думаю, cheat sheet по новому языку можно закончить. Как вы могли убедиться, семантически Eel очень близок к эрлангу. Этим он отличается, например, от эликсира, который ‑тоже, как бы, эрланг с синтаксисом Ruby, но делает всё по‑своему.
От семантической близости к эрлангу польза, конечно, есть. Если мы захотим форкнуть какой‑нибудь эрланговский language server и адаптировать его для своего языка, это будет сделать намного проще. На самом деле, я уже попробовал, ради любопытства, форкнуть elp, и DeepSeek справился без каких‑то особых проблем.
CPython
В предыдущем посте я писал о якобы одном языке для двух рантаймов: CPython и BEAM. Признаю — эта идея была слишком идеалистичной: если рантаймы разные — скорее всего, и языки будут отличаться.
В итоге, я решил не менять семантику CPython, а фичи для него — сильно уменьшить в размерах. Так что они теперь вполне подходят по размерам для нескольких PEP.
Главную фичу — паттерн матчинг — я решил сделать частью assert:
assert Response(status=200, json={'items': items}) = resp if items
В таком варианте, она, во‑первых, почти не конфликтует с остальным питоном, а во‑вторых — очень полезна в тестах, где ассерты, как раз, в основном и используются.
После ассерта можно писать всё, что можно писать после match..case. Ну — и просто булево условие, как в обычных ассертах. Даже немного больше:
assert pattern1, pattern2, ... if <условия>
Сделано это, чтобы можно было писать ассерты после декларации функций, аказывая паттерны для переменных.
def mycontroller(method: str, data: dict): assert 'GET' | 'POST' = method, {'user': user, 'order': order} = data
match def я тоже оставил. Не то, чтобы он особенно был нужен, но у него — ровно такой же синтаксис, как и у assert. И пайплайны — оставил тоже:
odd_numbers = range(10) ..filter: lambda x: x % 2 != 0
Без других изменений — я решил, что можно обойтись.
match — решил оставить ровно таким же, как в питоне — он не деструктивный и не возвращает значение. Так что, можно смотреть на это и делать PEP в питон для любой понравившейся фичи.
Правда, насчёт форка CPython я передумал. Это не обязательно, это вполне может быть сторонним пакетом. Так я и сделал: вместо форка питона — пакет eelang:
uv add eelang
Только файлы теперь нужно называть с суффиксом .eel:
python -m eel mymodule.eel
Но, чтобы не публиковать пакет ради пакета, я нашёл ему применение. Пайплайны отлично сочетаются с ассертами для тестов, особенно таких:
сделать запрос
проверить статус и базовую структуру ответа
Поэтому я опубликовал второй пакет, httpee:
uv add httpee
Он позволяет делать именно это:
from httpee import GET, httpee, Response httpee() ..GET: 'https://api.example.com/users' ..assert: Response(status=200)
Основан он на хипстерском пакете fetch.py от Kenneth Reitz. Это http‑клиент в одном файле. Так что, есть все основания его использовать.
Также, я форкнул ruff и его плагин для редактора Zed, чтобы они поддерживали синтаксис .eel. Но это, конечно, ради любопытства. И, конечно, не я, а DeepSeek. Всё отлично работает — подсветка, автокомплит и всё, что положено.
Но это всего лишь proof‑of‑concept. Я думаю, плагин для IDE и language server будет использовать те же технологии и библиотеки, что и язык Eel для BEAM. В целом, фичи для CPython намного меньше по размеру, чем язык для BEAM, поэтому отлично подходят для обкатки технологий.
Дальнейшие планы
Вообще, наверное, самый прямолинейный путь, чтобы сделать компилятор для Eel — это парсить код (модифицированным) парсером питона и генерировать «абстрактный эрланг». Но как‑то стало принято для современных языков предоставлять из коробки плагины для IDE, чаще всего — language server. А для этой цели вышеуказанный прямолинейный путь не годится от слова совсем.
Вообще, в последнее время, прямо какой‑то бум LSP и статических анализаторов. Например, только для питона есть ruff/ty, pyrefly, zuban — это только, если брать новые. И почему‑то все написаны на Rust. Вот это лично мне непонятно, лично я бы взял плагины для Intellij и писал бы на Kotlin. Кстати, долгое время компиляции — это очевидный и большой минус Rust при разработке с помощью агента. Да, наверное, и без него.
Но это пока поле для исследования. Хотя я, скорее всего, склоняюсь к тому, чтобы использовать платформу JetBrains/PyCharm. Кстати: сейчас они в процессе перехода на EelApi для работы с удалёнными окружениями. Смешно.