Snake? Eel! Мои эксперименты с питоном и эрлангом продолжаются

Дорогие читатели-питонисты, хочу вас поздравить с выходом питона 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)) # кортеж, а не генератор, как в питонеОпциональная статическая типизация
Киллер-фича и новинка сезона!
Уж не знаю, что такого в этой статической типизации, но она вдруг всем стала нужна. Тем не менее, посколько синтаксис для функций взят из питона, параметры функций можно аннотировать типами. И им ничего не мешает превращаться в эрланговские 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} = datamatch def я тоже оставил. Не то, чтобы он особенно был нужен, но у него - ровно такой же синтаксис, как и у assert. И пайплайны, конечно, оставил тоже:
odd_numbers =
range(10)
..filter:
lambda x: x % 2 != 0Без других изменений - я решил, что можно обойтись. match - решил оставить ровно таким же, как в питоне - он не деструктивный и не возвращает значение.
Так что, можно смотреть на это и делать PEP в питон для любой понравившейся фичи. Но насчёт форка CPython я передумал. Это не обязательно, это вполне может быть сторонним пакетом. Я так и сделал:
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 для работы с удалёнными окружениями. Смешно.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.