В одной из предыдущих статей я рассказывал о языке 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. Конечно же, ещё напишу об успехах.
Опрос, как и обещал — про синтаксис, а вот нужен ли этот язык вообще — об этом напишите в комментариях!
ссылка на оригинал статьи https://habr.com/ru/articles/732586/
Добавить комментарий