Начало работы

В этой главе мы установим Gleam, создадим первый проект и познакомимся с базовыми типами.

Цели главы

В этой главе мы:

  • Установим Gleam и Erlang/OTP
  • Создадим первый проект и разберём его структуру
  • Познакомимся с базовыми типами: Int, Float, String, Bool
  • Научимся объявлять функции и использовать модули
  • Решим задачу Эйлера №1

Установка

Gleam

Gleam можно установить несколькими способами:

# macOS (Homebrew)
$ brew install gleam

# С помощью asdf
$ asdf plugin add gleam
$ asdf install gleam latest
$ asdf global gleam latest

# Из исходников (нужен Rust)
$ cargo install gleam

asdf — универсальный менеджер версий, который позволяет переключаться между версиями Gleam в разных проектах. cargo install собирает Gleam из исходников — занимает больше времени, но не требует отдельного инструмента.

Erlang/OTP

Gleam компилирует код в Erlang, поэтому для запуска нужна виртуальная машина BEAM:

# macOS (Homebrew)
$ brew install erlang

# С помощью asdf
$ asdf plugin add erlang
$ asdf install erlang latest
$ asdf global erlang latest

Проверим, что всё установлено:

$ gleam --version
gleam 1.14.0

$ erl -eval 'erlang:display(erlang:system_info(otp_release)), halt().' -noshell
"27"

Если оба вывода показывают версию — окружение настроено правильно. Gleam требует OTP 27+ для полноценной работы.

Первый проект

Создадим новый проект:

$ gleam new hello_gleam
Your Gleam project hello_gleam has been successfully created.
The project is in the hello_gleam directory.

$ cd hello_gleam

gleam new создаёт готовую структуру проекта с конфигурацией, пустым модулем и тестовым файлом. Имя проекта используется как имя пакета в Hex.

Структура проекта

hello_gleam/
├── gleam.toml          # Конфигурация проекта
├── src/
│   └── hello_gleam.gleam   # Исходный код
└── test/
    └── hello_gleam_test.gleam  # Тесты

Файл gleam.toml — сердце проекта:

name = "hello_gleam"
version = "1.0.0"
target = "erlang"

[dependencies]
gleam_stdlib = ">= 0.44.0 and < 2.0.0"

[dev-dependencies]
gleeunit = ">= 1.0.0 and < 2.0.0"

Здесь указано имя проекта, таргет (Erlang по умолчанию) и зависимости. gleam_stdlib — стандартная библиотека, gleeunit — тестовый фреймворк.

Запуск и тестирование

# Скомпилировать и запустить
$ gleam run
Hello from hello_gleam!

# Запустить тесты
$ gleam test
Compiled in 0.02s
Running hello_gleam_test.main
1 tests, 0 failures

gleam run компилирует проект и вызывает функцию main из основного модуля. gleam test компилирует тесты и запускает gleeunit.main().

Другие полезные команды

# Добавить зависимость
$ gleam add gleam_json

# Отформатировать код
$ gleam format

# Собрать без запуска
$ gleam build

# Проверить типы
$ gleam check

# Сгенерировать документацию
$ gleam docs build

gleam format — не опция, а стандарт: весь Gleam-код форматируется единообразно.

Базовые типы

В Gleam четыре примитивных типа:

Int

Целые числа произвольной точности (на BEAM-таргете):

let x = 42
let big = 1_000_000  // подчёркивания для читаемости
let hex = 0xFF       // шестнадцатеричный литерал
let oct = 0o77       // восьмеричный
let bin = 0b1010     // двоичный

Арифметические операторы для целых чисел: +, -, *, / (целочисленное деление), % (остаток от деления).

let sum = 2 + 3      // 5
let diff = 10 - 4    // 6
let prod = 3 * 7     // 21
let quot = 10 / 3    // 3 (целочисленное деление)
let rem = 10 % 3     // 1

/ в Gleam — всегда целочисленное деление для Int. Деление на ноль не выбросит исключение — на BEAM оно вернёт ошибку в рантайме.

Float

Числа с плавающей точкой (64-бит IEEE 754):

let pi = 3.14159
let negative = -0.5
let scientific = 1.0e10

Важно: операторы для Float отличаются от операторов для Int. К ним добавляется точка:

let sum = 2.0 +. 3.0     // 5.0
let diff = 10.0 -. 4.0   // 6.0
let prod = 3.0 *. 7.0    // 21.0
let quot = 10.0 /. 3.0   // 3.333...

Это принципиальное решение: Gleam не допускает неявного приведения типов. Нельзя сложить Int и Float напрямую:

// ✗ Ошибка компиляции!
let result = 2 + 3.0

// ✓ Нужно явно сконвертировать
import gleam/int
let result = int.to_float(2) +. 3.0

Отсутствие неявных приведений — осознанное решение: ошибки типов обнаруживаются компилятором, а не в рантайме. Это устраняет целый класс трудноуловимых багов.

String

Строки в Gleam — последовательности UTF-8, заключённые в двойные кавычки:

let greeting = "Привет, мир!"
let empty = ""

Конкатенация строк — оператор <>:

let name = "Gleam"
let message = "Привет, " <> name <> "!"
// "Привет, Gleam!"

Gleam не поддерживает интерполяцию строк — только конкатенацию. Чтобы вставить число в строку, нужно сначала сконвертировать:

import gleam/int

let age = 25
let message = "Мне " <> int.to_string(age) <> " лет"

int.to_string — стандартный способ вставить число в строку. Отсутствие интерполяции компенсируется явностью: всегда видно, что именно конвертируется.

Bool

Логические значения — True и False:

let is_active = True
let is_admin = False

Операторы сравнения: ==, !=, <, >, <=, >=. Логические операторы: && (И), || (ИЛИ), ! (НЕ).

let a = True && False   // False
let b = True || False   // True
let c = !True           // False

Операторы && и || — ленивые: правая часть не вычисляется, если результат уже определён по левой.

let-привязки

В Gleam все значения неизменяемые. Ключевое слово let создаёт привязку имени к значению:

let x = 42
let y = x + 1  // 43

Повторная привязка (shadowing) допустима:

let x = 10
let x = x + 1  // x = 11, предыдущее значение затенено

Gleam требует, чтобы все привязки использовались. Для неиспользуемых значений есть два варианта:

// Полностью отбросить значение
let _ = some_function()

// Дать имя с подчёркиванием — значение не используется,
// но имя документирует намерение
let _result = some_function()

_ — анонимный discard, он не создаёт привязки и не попадет в рантайм. _result — именованный discard: компилятор не будет предупреждать о неиспользовании, но значение доступно (например, для отладки).

Блоки

Блок { ... } — группа выражений, результат блока — последнее выражение:

let result = {
  let a = 10
  let b = 20
  a + b
}
// result = 30

Блоки полезны для промежуточных вычислений внутри let.

Аннотации типов

Gleam выводит типы автоматически, но аннотации можно указать явно:

let name: String = "Gleam"
let count: Int = 42
let pi: Float = 3.14159

Аннотации не меняют поведение — требуются крайне редко, лишь документируют намерение и помогают компилятору выдавать более понятные ошибки.

Функции

Объявление функций

Функции объявляются с помощью fn. Ключевое слово pub делает функцию публичной (видимой из других модулей):

// Приватная функция (видна только в этом модуле)
fn square(x: Int) -> Int {
  x * x
}

// Публичная функция
pub fn greet(name: String) -> Nil {
  io.println("Привет, " <> name <> "!")
}

Тело функции — блок. Возвращаемое значение — последнее выражение, ключевое слово return отсутствует.

Типы параметров и возвращаемого значения указываются явно в сигнатуре.

Тип Nil

Nil — аналог void/unit в других языках. Используется, когда функция выполняет побочный эффект (например, печать) и не возвращает полезного значения:

pub fn greet(name: String) -> Nil {
  io.println("Привет, " <> name <> "!")
}

Функция с возвращаемым типом Nil выполняет побочный эффект (вывод на экран) и не несёт полезного значения. Nil — единственное значение типа Nil, аналог unit в Haskell или void в Rust.

Модули и импорты

Каждый файл .gleam — отдельный модуль. Имя модуля соответствует пути к файлу: src/math/utils.gleam → модуль math/utils.

Импорт модулей

// Импорт модуля — используем через квалифицированное имя
import gleam/io
io.println("Привет!")

// Импорт конкретных функций — используем без префикса
import gleam/io.{println}
println("Привет!")

// Импорт с переименованием
import gleam/io as console
console.println("Привет!")

Квалифицированный импорт (gleam/io) — самый распространённый: функции вызываются через io.println. Неквалифицированный ({println}) удобен для часто используемых функций. Псевдоним (as) полезен при конфликте имён.

Импорт типов

Типы из других модулей можно использовать через квалифицированное имя (option.Option) или импортировать напрямую с ключевым словом type:

import gleam/option.{type Option}

// Теперь можно писать Option вместо option.Option
pub fn default_name(name: Option(String)) -> String {
  option.unwrap(name, "Аноним")
}

В Gleam принято импортировать типы неквалифицированно ({type Option}), а функции вызывать через имя модуля (option.unwrap). Это позволяет сразу видеть, откуда пришла функция, при этом типы не перегружают сигнатуры.

Стандартная библиотека

Стандартная библиотека Gleam (gleam_stdlib) содержит модули для работы с основными типами:

  • gleam/io — вывод в консоль (println, debug)
  • gleam/int — операции с целыми числами (to_string, to_float, parse, sum, max, min, is_even, is_odd, absolute_value)
  • gleam/float — операции с числами с плавающей точкой (to_string, parse, round, floor, ceiling, power, square_root)

Пример использования:

import gleam/int
import gleam/float
import gleam/io

pub fn main() {
  // gleam/io
  io.println("Привет!")
  io.debug(42)         // выводит значение любого типа

  // gleam/int
  let n = 42
  io.println(int.to_string(n))           // "42"
  io.debug(int.is_even(n))               // True
  io.debug(int.max(10, 20))              // 20

  // gleam/float
  let x = 9.0
  io.debug(float.square_root(x))         // Ok(3.0)
  io.debug(float.round(3.7))             // 4
  io.debug(float.to_precision(3.14159, 2))  // 3.14
}

io.debug выводит значение любого типа — полезно для отладки. В продакшене предпочтительнее io.println с явным to_string.

Конвертация типов

В Gleam нет неявных приведений типов. Все конвертации — явные:

import gleam/int
import gleam/float

// Int → Float
let x = int.to_float(42)    // 42.0

// Float → Int
let y = float.round(3.7)    // 4
let z = float.floor(3.7)    // 3
let w = float.ceiling(3.2)  // 4
let t = float.truncate(3.9) // 3

// Int → String
let s = int.to_string(42)   // "42"

// String → Int
let n = int.parse("42")     // Ok(42)
let e = int.parse("abc")    // Error(Nil)

Обратите внимание: int.parse возвращает Result(Int, Nil), а не Int. Если строка не является числом, мы получаем Error(Nil) вместо исключения. Подробнее о Result — в главе 5.

Проект: задача Эйлера №1

Найти сумму всех натуральных чисел меньше 1000, которые делятся на 3 или 5.

Решим эту задачу, используя изученные концепции:

import gleam/int
import gleam/io
import gleam/list

pub fn euler1(limit: Int) -> Int {
  list.range(1, limit)
  |> list.filter(fn(x) { x % 3 == 0 || x % 5 == 0 })
  |> int.sum
}

pub fn main() {
  let answer = euler1(1000)
  io.println("Ответ: " <> int.to_string(answer))
  // Ответ: 233168
}

Не беспокойтесь, если вам пока незнакомы list.range, list.filter или оператор |> — мы подробно разберём их в следующих главах. Пока достаточно понять общую идею:

  1. list.range(1, limit) создаёт список чисел от 1 до limit (не включая limit)
  2. list.filter(...) оставляет только числа, делящиеся на 3 или 5
  3. int.sum складывает все оставшиеся числа
  4. |> передаёт результат одной функции на вход следующей

Упражнения

Решения упражнений пишите в файле exercises/chapter02/test/my_solutions.gleam. Запускайте тесты командой:

cd exercises/chapter02
gleam test

Команды выше позволяют перейти в папку упражнения и запустить тесты для проверки ваших решений.

1. Диагональ прямоугольника (Лёгкое)

Напишите функцию diagonal, которая вычисляет длину диагонали прямоугольника по двум сторонам.

pub fn diagonal(a: Float, b: Float) -> Float

Формула: \( d = \sqrt{a^2 + b^2} \)

Примеры:

diagonal(3.0, 4.0) == 5.0
diagonal(5.0, 12.0) == 13.0

Подсказка: используйте float.square_root из gleam/float. Функция возвращает Result(Float, Nil) — используйте let assert Ok(result) = ... для извлечения значения (мы подробно разберём Result позже).

2. Конвертация температуры (Лёгкое)

Напишите функцию celsius_to_fahrenheit, которая переводит градусы Цельсия в Фаренгейт.

pub fn celsius_to_fahrenheit(c: Float) -> Float

Формула: \( F = C \times \frac{9}{5} + 32 \)

Примеры:

celsius_to_fahrenheit(0.0) == 32.0
celsius_to_fahrenheit(100.0) == 212.0

Примеры выше показывают ожидаемые результаты конвертации: 0 °C соответствует 32 °F, а 100 °C — 212 °F.

3. Обратная конвертация (Лёгкое)

Напишите функцию fahrenheit_to_celsius, обратную к предыдущей.

pub fn fahrenheit_to_celsius(f: Float) -> Float

Примеры:

fahrenheit_to_celsius(32.0) == 0.0
fahrenheit_to_celsius(212.0) == 100.0

Примеры выше демонстрируют обратное преобразование: 32 °F — это 0 °C, а 212 °F — 100 °C.

4. Задача Эйлера (Среднее)

Напишите функцию euler1, которая находит сумму всех натуральных чисел меньше n, делящихся на 3 или 5.

pub fn euler1(n: Int) -> Int

Примеры:

euler1(10) == 23       // 3 + 5 + 6 + 9 = 23
euler1(1000) == 233168

Подсказка: используйте list.range, list.filter и int.sum.

Заключение

В этой главе мы:

  • Установили Gleam и Erlang
  • Создали проект и разобрались в его структуре
  • Познакомились с четырьмя базовыми типами и их операторами
  • Научились объявлять функции и импортировать модули
  • Увидели, что Gleam не допускает неявных приведений типов

В следующей главе мы глубже изучим функции: анонимные функции, pipe-оператор, use-выражения и pattern matching.