· 10 мин 👁 1.3k Начинающий

Язык Gleam — первые шаги

Hello world, модули, импорты и система типов: разбираем основы Gleam на живых примерах и сравниваем с тем, как это устроено в Go и Java.

gleamhello worldмодулитипыимпорты
Содержание

Начнём с самого начала — с программы, которая выводит строку в консоль.

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 выводит значение любого типа и предназначен только для отладки