Pull to refresh

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

Level of difficultyMedium
Reading time5 min
Views2.7K

В одной из предыдущих статей я рассказывал о языке 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. Конечно же, ещё напишу об успехах.

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

Only registered users can participate in poll. Log in, please.
Как вам ягодный синтаксис?
21.05% Что-то в нём есть4
78.95% Фуфло!15
19 users voted. 6 users abstained.
Tags:
Hubs:
Total votes 2: ↑2 and ↓0+2
Comments10

Articles