Erlang больше не в моде. berry-lang — новый язык для BEAM со статической типизацией

от автора

В одной из предыдущих статей я рассказывал о языке gleam и даже хвалил его. Это тоже язык для платформы BEAM, и он тоже подходит под описание, которое я сделал для berry-lang. Что ж — хочу сказать, что gleam не выдержал моего более пристального взгляда и разочаровал меня полностью.

Приведу пару примеров. Во-первых, gleam намеренно и без всякой причины ломает совместимость с эрлангом. Взять, например, атомы: gleam их не поддерживает. Однако, большая часть API эрланга их использует — получается, нет совместимости. Причём, синтаксис для атомов из эрланга — имя в одинарных кавычках — в gleam ничем не занят! Одинарные кавычки в нём просто запрещены.

То же самое — с поддержкой OTP. Автор gleam решил, что для обмена между процессами будут разрешены только типизированные сообщения, и что он для этого сделает свой API. В итоге, получилось не очень — он выделил это в отдельный репозиторий и сказал, что OTP будет опциональной частью (она недоделана и вообще странная). Как OTP может быть опциональной для эрланга — непонятно. Зато, автор добавил Javascript как второй таргет, помимо эрланга. В общем, вы понимаете, почему я разочаровался.

Ещё есть Elixir. С синтаксисом у него всё более-менее нормально. Однако, семантически он не на 100% соответствует эрлангу. Например, в нём есть макросы, потом — есть struct-ы, которые под капотом — map, причём модулю соответствует struct. Почему модулю должен соответствовать map — непонятно. Ну, то есть, понятно: видимо, сказалась тяга автора к ООП.

Но больше всего в Elixir, конечно, бесят макросы. В документации сказано, что они должны использоваться только в самых исключительных случаях. Но, конечно, каждый считает, что его случай именно такой. Сам автор суёт макросы чуть ли не в каждую свою библиотеку. В целом, Elixir — отличный язык, но у него один большой недостаток: он притягивает к себе низкокачественный код.

Наконец, есть Erlang — у него винтажный ретро-синтаксис. Не думаю, что сами core разработчики от него в восторге — просто так исторически сложилось, что он с самого начала практически не менялся — а сейчас начинать менять его никто не хочет. В общем, это пример того, что может стать с языком программирования, если его будет развивать не Mozilla, а Ericsson.

Но давайте поговорим, наконец, о ягодах.

Стоит ли говорить, что придумывать новый синтаксис — дело неблагодарное и, лично мне, совсем не хотелось. Я думал взять за основу синтаксис питона, сделав поправку на то, что язык должен быть функциональным. Уже искал, какой лучше взять парсер и всё остальное — ничего нормального не было. Питоновский парсер написан на си и на питоне — не очень годится. Есть парсер и линтер на Rust — но к Rust я отношусь довольно прохладно.

В этих поисках, я совершенно случайно наткнулся на Сyber — «fast, efficient, and concurrent scripting language». Написан он на zig (отличный выбор!). Сyber пока далёк от применения в продакшне, но я желаю ему стать отличным скриптовым языком.

Но будущее Сyber — это одно, а ведь нам от него нужен только синтаксис. Так вот, синтаксис у Сyber — на удивление, хорош! Очень радует, что он смог утащить из питона, так сказать, не букву закона, а его дух.

Вот как, например, выглядит цикл for:

for 0..100 each i:     print i    -- 0, 1, 2, ... , 99

Как видите, синтаксис отличается от питона. Хотя и во многом совпадает.

Для импортов и объявления функций Сyber использует синтаксис го, а не питона:

import {sqrt} 'math'  func dist(x0 int, y0 int, x1 int, y1 int) number:     dx = x0 - x1     dy = y0 - y1     return sqrt(dx^2 + dy^2)

По мне, так двоеточие между переменной и типом — чуть более читаемо. Но, для моего случая, отсутствие двоеточия — ещё лучше. Почему — увидите позже. А вот возвращаемое значение мы всё-таки будем отделять символами ->:

func sum(x int, y int) -> int:     x + y

Дело в том, что аннотации типами и другие guards могут стоять и после объявления аргументов — то есть, после скобок:

func my_list_function([head | tail]) head int -> list:

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

Расскажу о статической типизации. Кстати, как ни странно, её в эрланге довольно часто используют, несмотря на плохо приспособленный синтаксис (аннотация -spec). Моя версия придумана специально для эрланга: паттерн-матчинг учитывает типы, так что guards обычно писать не нужно.

Так, предыдущий пример будет соответствовать следующему:

my_list_function([Head | Tail]) when is_integer(Head) -> 

Но это ещё не всё: после типов в скобках могут стоять условия:

func my_fun(m map(size>0)):

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

my_fun(M) when is_map(M), map_size(M) > 0 ->

Guards в эрланге обычно логически привязаны к типу, и для каждого типа их немного — до 10, в лучшем случае. Указание их в скобках, мне кажется, хорошо читаемо — плюс, обеспечивает лёгкую возможность автокомплита.

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

func my_fun((name, value)) name atom, value int:

Здесь, my_fun принимает tuple, состоящий из атома и целого числа. Теперь вы видите, почему хорошо, что между переменной и её типом нет двоеточия? Потому что двоеточие стоит в конце.

Для оператора case всё-таки нужен разделитель when перед guards:

match x:     []:         none     [head | tail] when head int:         throw "Not implemented" 

Да, Сyber использует match, а не case — ну, пусть будет match.

Кастомные типы, конечно, объявлять можно и нужно. У Cyber для этого такой синтаксис:

type Student record:      -- for Erlang records     name string     age int     gpa number  type Professor map:       -- for maps     name string     age int     known_info Info

Дух «значимых пробелов» (significant whitespace) в Cyber выдержан везде. Например, в нём есть полноценные лямда-функции — которых в питоне, между прочим, нет.

Pipeline-оператора — увы, нет. Всё-таки, Cyber — не функциональный язык. Но вот, как он, теоретически, мог бы выглядеть:

filtered =     range(10)     ..filter func(val):      -- pipeline operator ..         val % 2 == 0         -- even number

Что также эквивалентно

filtered =     range(10)     ..filter(_) func(val):    -- placeholder _ is for lambda         val % 2 == 0          

Идея использовать .. в качестве пайплайн-оператора, честно говоря, навеяна синтаксисом Cyber. Две точки + имя функции — это как бы частичная функция, привязанная к предыдущему результату — мне нравится.

Для передачи лямды в качестве параметра используем placeholder _:

filtered = filter(range(10), _) func(val): val % 2 == 0

В случае, если лямда — это единственный аргумент, placeholder можно не писать:

at0 = func(f): f(0)  f0 = at0 func(x):     math.sin(x)  -- or the pipeline version: f0 =     func(x):         math.sin(x)     ..at0()

В целом — мне кажется, синтаксис получается симпатичный, а как вам? По этому вопросу можно проголосовать в опросе.

Пару слов о реализации: мне нравится zig (без иронии), но новый язык я собираюсь писать на нём самом — like a boss. Это называется self-hosted. Конечно же, ещё напишу об успехах.

Опрос, как и обещал — про синтаксис, а вот нужен ли этот язык вообще — об этом напишите в комментариях!

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

Как вам ягодный синтаксис?

21.05% Что-то в нём есть4
78.95% Фуфло!15

Проголосовали 19 пользователей. Воздержались 6 пользователей.

ссылка на оригинал статьи https://habr.com/ru/articles/732586/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *