Если вы никогда не видели кода на gleam, то выглядит он примерно так. Пример взят из стандартной библиотеки. Стандартная библиотека, кстати, довольно полная, но ещё не совсем документирована. Поэтому, если захотите ей пользоваться, придётся немного смотреть в исходный код.
Синтаксис, наверно, ближе всего к Rust. В нём есть все прелести функционального программирования, например, отсутствует конструкция return.
Код разбит на модули и функции. Модули соответствуют файлам-исходникам с интуитивно понятным синтаксисом для импорта. Этим он отличается, скажем, от языка Elixir, где физические и логические модули - это не одно и тоже. Кроме этого, в эликсире модулям соответствуют структуры данных (struct), что вносит ещё больше путаницы.
Язык довольно минималистичный. Например, в нём отсутствуют for
и if
. Также отсутствует синтаксис для словарей, так что pattern matching для них тоже не сделать. Интересующиеся могут почитать The Gleam Book, чтобы узнать, что в gleam есть, а чего нет.
Также, в gleam отсутствуют "атомы". Erlang использует атомы как константы. Например, при успешном завершении функция может вернуть атом ok
, а при не успешном - error
. В gleam для этих целей используются типы. Напрямую атомы создавать в gleam нельзя, хотите использовать атом - создавайте новый тип. Решение оригинальное и мне очень нравится. Например, для упомянутого случая это выглядит так:
let parsed = case parse_int("123") {
Error(e) -> io.println("That wasn't an Int")
Ok(num) -> num
}
Кстати, об ошибках. В gleam нет конструкции try..catch
(которая есть в эрланге). Вместо этого, рекомендуется использовать вышеуказанный подход, напоминающий Rust. Вообще, в эрланге существует принцип "let it crash", который не рекомендует ловить исключения, вместо этого, позволяя процессу, в котором исключение возникло, крэшнуться. В принципе, можно сказать, что gleam следует рекомендациям, потому что в нём не существует в принципе возможности поймать исключение.
Функции - приватные по дефолту. Чтобы сделать публичную функцию, нужно ей указать модификатор pub
. Подойдёт для лиц, не стремящихся к публичности. Также есть простой и понятный способ для объявления констант на уровне модуля с помощью слова const
.
Есть ещё интересная конструкция use
, у которой, наверно, нет аналогов в других языках. Автор её описывает как синтаксический сахар для функций-обёрток. Только на стероидах. И приводит такой пример
pub fn main() {
use <- logger.record_timing
use db <- database.connect
use f <- file.open("file.txt")
// Do with something here...
}
Это эквивалентно следующему коду
pub fn main() {
logger.record_timing(fn() {
database.connect(fn(db) {
file.open("file.txt", fn(f) {
// Do with something here...
})
})
})
}
В частности, это один из способов решить проблему callbacks hell.
Ну и, конечно, нужно отдельно сказать про типы. Они в gleam опциональные, их можно не объявлять. Можно аннотировать любое выражение типом. Кастомные типы объявляются так
type User {
LoggedIn(name: String)
Guest
}
Как видите, можно объявлять несколько типов внутри одного блока type. Но - можно и один. Использовать их потом можно так
fn get_name(user) {
case user {
LoggedIn(name) -> name
Guest -> "Guest user"
}
}
То есть, типы используются как при pattern-matching'е, так и для статического анализа. Есть также ограничения, которые gleam накладывает на структуры данных: в списках и словарях нельзя иметь значения разных типов. Компилятор запрещает.
По поводу этих последних ограничений - я думаю, что они довольно спорные. Ну, выдавал бы ворнинг, а окончательное решение было бы за программистом. В конце концов, сам эрланг не типизированный, а аннотация типами в gleam опциональная - у компилятора gleam может просто не быть всей нужной информации.
Отдельно хочу отметить, что в gleam отсутствуют возможности для сумасшедшего мета-программирования, которые есть, например, в языке Elixir (макросы). Что меня очень радует: макросы для высокоуровневого языка - это однозначное зло. Хотя в системных языках выполнять код во время компиляции бывает нужно, и это совершенно нормально. Такие возможности есть, например, в языках Zig, Rust, C++.
gleam при сборке делает source-to-source преобразование своего кода в код на эрланге. Написан он на языке Rust.
Вот, это всё, что я вам хотел рассказать о языке gleam. В конце есть опрос. Придирчивый читатель может дальше не читать, потому что дальше будут предложения максимально не конкретные и максимально не соответствующие.
В общем, изучая возможности gleam, я поймал себя на мысли, что они целиком покрываются синтаксисом питона (я программист на питоне). Ну, или почти целиком: пайплайн-оператора разве что в питоне нет (этого: |>
).
Так что, я подумал, что если какой-нибудь, не вполне адекватный, человек решит переделать gleam под питоновский синтаксис, то не встретит на этом пути никаких трудностей. Разумеется, такой человек стал бы использовать компилятор gleam для генерации кода на эрланге.
Возможно ли это, и насколько императивный язык отличается от функционального?
В версии 3.10 в питон добавили match..case
с довольно развесистым синтаксисом: с типами, условиями и всем, что только можно было добавить. Выглядит он так:
match user:
case LoggedIn(name):
# serve user
case Guest():
# serve guest
Годится ли он? Не совсем. Есть небольшая проблема: match
должен быть выражением, должен возвращать значение. То есть, должно быть так
result = match user:
# и дальше по тексту
То же самое касается других ключевых слов: if, for, try. Только в том случае, если блок match
стоит последним в функции, годится его текущий синтаксис.
Про классы и весь ООП можно забыть (и нужно). Классы должны использоваться только для аннотации типов. В остальном же - ванильный питоновский синтаксис вполне самодостаточен. Можно брать парсер для питона и парсить им код.
Но не бывает так, чтобы у кого-то была возможность добавить ещё синтаксис, и он этого не сделал. Если это случится в нашем случае, то что это будет? Вот возможные варианты.
1) Pipeline operator (|>
)
Уж очень хорошо в коде смотрится. Наиболее полезен в конце выражений match, if, for:
result = match obj:
case {'data': data}: data
|> process_data()
|> process_more()
Также блок match может сам стоять после оператора пайплайна:
result = match obj:
# что-то
|> match:
# ещё что-то
В других случаях он менее нужен, но, конечно, почему бы не использовать его и в обычных выражениях.
2) Анонимные функции
В питоне есть лямды, но они неполноценные. Полноценные бы выглядели так:
f = def(x, y):
x + y
Вообще-то, это то же самое, что объявить функцию обычным способом. Зато лямду можно использовать после оператора пайплайна:
result = match value:
# что-то
|> def(x):
# что-то на выходе
3) Pattern matching на операции присваивания
MyType(data) = obj
Это удобно - иметь такую возможность. Синтаксис - такой же, как в части case
у match
.
Таким образом, можно с уверенностью заключить, что gleam вполне мог бы иметь синтаксис питона. Надеюсь, я удовлетворил ваше любопытство в плане, как бы это могло быть, и убедил в том, что синтаксис у питона действительно богат.
Что вы скажете?
Комментарии (16)
dopusteam
00.00.0000 00:00Есть ещё интересная конструкция use, у которой, наверно, нет аналогов в других языках
Выглядит как using в последних версиях c#
wvladw92
Какие у этого яп преимущества? Позволяет более продуктивно решать какие-то задачи?
abetkin Автор
нет, но я верю в силу пробелов (то есть, отступов)
abetkin Автор
а у gleam преимущества очевидные: статическая типизация и платформа эрланга
Hungryee
Мне кажется, подразумевался вопрос «Зачем он вообще нужен»
abetkin Автор
Ну платформа эрланга довольно уникальная, она одна такая) BEAM называется. Для распределённых систем в основном используется и серверного кода. Языки со статической типизацией, кроме gleam, для неё существуют, но они больше на Haskell похожи
gibson_dev
Фактически это просто еще один язык для виртуальной машины BEAM (основной язык - эрланг), умеет все тоже самое но более чистый и лаконичный синтаксис. В принципе тоже самое что и Elixir