Язык Gleam — первые шаги
Hello world, модули, импорты и система типов: разбираем основы Gleam на живых примерах и сравниваем с тем, как это устроено в Go и Java.
Содержание
Начнём с самого начала — с программы, которая выводит строку в консоль.
Hello world
import gleam/io
pub fn main() {
io.println("Hello, Joe!")
}
Запускается командой gleam run.
Три вещи сразу бросаются в глаза, если вы пришли из Go или Java.
Первое: pub fn вместо func или public static void. pub означает, что функция видна за пределами модуля, fn — что это функция. Без pub функция приватная — доступна только внутри своего модуля.
Второе: нет точки с запятой. Gleam, как и Go, не требует ; в конце строки. Компилятор сам понимает, где заканчивается выражение.
Третье: нет return. В Gleam каждая функция возвращает значение последнего выражения. io.println возвращает Nil — аналог void, но явный тип, а не отсутствие типа. Если привыкнуть, это читается естественно: функция есть своё последнее выражение.
В Go то же самое выглядело бы так:
package main
import "fmt"
func main() {
fmt.Println("Hello, Joe!")
}
Структурно похоже: импорт, main, вызов функции из пакета. Разница в деталях: в Go main не может быть pub — видимость пакетов устроена иначе. В Gleam main явно помечается как публичная.
Модули
Весь код в Gleam живёт в модулях. Модуль — это файл. Файл io.gleam в директории gleam/ — это модуль gleam/io. Имя модуля соответствует пути к файлу.
import gleam/io
import gleam/string as text
pub fn main() {
// Функция из модуля gleam/io
io.println("Hello, Mike!")
// Функция из модуля gleam/string, переименованного в text
io.println(text.reverse("Hello, Joe!"))
}
Ключевое слово as позволяет дать модулю другое имя. Здесь gleam/string доступен как text. Это удобно, когда имя модуля длинное или конфликтует с чем-то в вашем коде.
Комментарии начинаются с // и пишутся над тем, что комментируют — не рядом. Это соглашение языка, не просто стиль.
В Go модули называются пакетами, и механика похожа:
import (
"fmt"
str "strings"
)
str.ToUpper("hello") // псевдоним работает так же
В Java аналог — import static и псевдонимы через import com.example.Foo as Bar появились только в последних версиях. Исторически в Java с конфликтами имён боролись по-другому: просто писали полное имя класса прямо в коде — java.util.Date вместо Date.
Важная деталь: в Gleam после импорта модуль называется по последней части пути. gleam/io — это io. gleam/string — это string. Это предсказуемо и не требует запоминать алиасы, если они не нужны.
Неквалифицированные импорты
Обычно функции используются с именем модуля перед ними: io.println(...). Это называется квалифицированным вызовом — сразу видно, откуда функция.
Можно импортировать функцию напрямую и использовать без префикса:
import gleam/io.{println}
pub fn main() {
// Квалифицированный вызов
io.println("This is qualified")
// Неквалифицированный вызов
println("This is unqualified")
}
Официальная рекомендация Gleam: предпочитайте квалифицированные импорты. Когда в файле несколько импортов, неквалифицированный вызов println(...) не даёт понять, откуда эта функция — нужно идти наверх файла и искать.
В Go неквалифицированных импортов нет вообще — всегда fmt.Println, никогда просто Println. Исключение — пакет testing в тестах, но это отдельная история.
В Java import static делает ровно то же самое:
import static java.lang.Math.sqrt;
sqrt(16); // вместо Math.sqrt(16)
И там та же проблема читаемости: в большом файле с несколькими import static непросто понять, откуда пришла та или иная функция.
Когда неквалифицированный импорт оправдан? Когда функция используется очень часто и её происхождение очевидно из контекста. Например, в тестах часто импортируют should.equal напрямую — потому что это единственная функция-ассерт и её смысл понятен без префикса.
Система типов
import gleam/io
pub fn main() {
io.println("My lucky number is:")
// io.println(4)
// 👆 Раскомментируйте, чтобы увидеть ошибку компилятора
}
io.println принимает String. Если передать 4 — компилятор откажется собирать программу и покажет ошибку с точным указанием места и типа проблемы.
Это принципиальное отличие от динамически типизированных языков: ошибка обнаруживается до запуска, а не в момент, когда она уже произошла в боевой системе.
В Gleam нет null — совсем. Нет неявных преобразований типов. Нет исключений. Если функция может вернуть отсутствующее значение, она возвращает Option(T). Если может упасть с ошибкой — Result(T, E). Компилятор заставляет обработать оба случая явно.
Для отладки есть ключевое слово echo — оно выводит значение любого типа:
import gleam/io
pub fn main() {
echo 42
echo "hello"
echo [1, 2, 3]
}
echo — не функция, а именно ключевое слово. Его нельзя передать как аргумент или присвоить переменной. Оно существует только для отладки и не должно оставаться в production-коде.
В Go система типов тоже статическая, но есть nil — и он применим к указателям, интерфейсам, слайсам, мапам и каналам. Разыменование nil-указателя — runtime panic, не ошибка компилятора. В Gleam такого класса ошибок не существует по определению.
В Java null применим к любому объекту, и NullPointerException — одна из самых распространённых ошибок в production. Java 14 добавила полезное сообщение об ошибке с указанием, какая именно переменная была null, но сам null никуда не делся. Kotlin решил это введением nullable-типов (String? vs String) — примерно в том же направлении, что и Gleam, но с обратной совместимостью с Java.
Сообщения об ошибках компилятора Gleam часто называют одними из лучших в индустрии — наравне с Rust и Elm. Ошибка не просто указывает строку, но объясняет, что ожидалось, что получено и как это исправить.
Итоги
- Весь код в Gleam живёт в модулях; имя модуля соответствует пути к файлу
pub fn— публичная функция, безpub— приватная- Точек с запятой нет;
returnне нужен — функция возвращает последнее выражение - Импорты квалифицированные по умолчанию; неквалифицированные возможны, но не рекомендуются
- Нет
null, нет исключений, нет неявных преобразований — всё проверяется до запуска echoвыводит значение любого типа и предназначен только для отладки