В одной из предыдущих статей я рассказывал о языке 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.
Но давайте поговорим, наконец, о ягодах.
![](https://habrastorage.org/getpro/habr/upload_files/f43/214/e48/f43214e48ca2bbcd92effc53850f8519.png)
Стоит ли говорить, что придумывать новый синтаксис - дело неблагодарное и, лично мне, совсем не хотелось. Я думал взять за основу синтаксис питона, сделав поправку на то, что язык должен быть функциональным. Уже искал, какой лучше взять парсер и всё остальное - ничего нормального не было. Питоновский парсер написан на си и на питоне - не очень годится. Есть парсер и линтер на 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. Конечно же, ещё напишу об успехах.
Опрос, как и обещал - про синтаксис, а вот нужен ли этот язык вообще - об этом напишите в комментариях!