За последние несколько месяцев я заметил, что в соцсети X все чаще появляются сообщения о языке Gleam, и решил попробовать поработать с ним. Этот опыт меня не разочаровал (если не считать нескольких моментов).
Дисклеймер 1. В этой статье описан мой личный опыт работы с языком. Это не всеобъемлющий обзор Gleam. Статью никто не спонсировал, и я не связан с командой Gleam.
Дисклеймер 2. У меня был небольшой опыт работы с Erlang VM, а также с функциональным программированием в целом.
Начало работы
Первым делом я прошел официальный вводный курс по языку Gleam. За час я ознакомился с синтаксисом и основными понятиями. Я помню, что именно так я начинал и с Go, и считаю, что у каждого языка должен быть такой интерактивный курс.
import gleam/io
pub fn main() {
io.println("Hello, Joe!")
}
Затем я установил язык на компьютер. Я был удивлен тем, насколько легко получилось установить язык и инструментарий на macos.
brew install gleam
brew install erlang
Вам также может понадобиться rebar3, хотя для моего проекта это оказалось не нужным.
brew install rebar3
Пока все шло хорошо!
Проект «Морская свинка»
Я предпочитаю учиться на практике, поэтому решил создать небольшой проект с помощью Gleam. Я хотел разработать простое daemon-приложение, которое отслеживает несколько сайтов одновременно из конфигурации Yaml и сохраняет результаты в базе данных SQLite.
Конфигурация выглядит примерно так:
websites:
- url: https://packagemain.tech
interval: 10
- url: https://pliutau.com
interval: 15
- url: https://news.ycombinator.com
interval: 30
pattern: gleam
Я выбрал этот проект, так как мог познакомиться со следующими аспектами:
- параллельная обработка;
- HTTP-вызовы;
- пакеты Gleam;
- типы данных;
- обработка ошибок;
- парсинг Yaml;
- тестирование.
Создание нового проекта:
gleam new websites_checker
Эта команда создает папку с заранее определенной структурой. Количество шаблонного кода было минимально, и я сразу же смог приступить к работе с кодом. В основном я сосредоточился на файлах src/websites_checker.gleam
и test/websites_checker_test.gleam
.
Зависимости и импорт
Здесь тоже все прошло гладко, так как в языке Gleam легко устанавливать зависимости и импортировать их в код. Для проекта потребовались следующие зависимости:
gleam add gleam_http
gleam add gleam_erlang
gleam add sqlight
gleam add glaml
gleam add simplifile
gleam add gleam_hackney
gleam add birl
Мне хотелось бы использовать меньше внешних зависимостей, но я не смог найти способ выполнения определенных действий (чтение файла, парсинг Yaml, работа со временем) с помощью только stdlib.
Зависимости фиксируются в gleam.toml
и могут быть загружены позже в CI/CD путем запуска следующей команды:
gleam deps download
Вы можете найти множество зависимостей в репозитории awesome-gleam, но многие из них совсем новые, и я бы не считал их готовыми к производству.
Парсинг Yaml
Здесь я столкнулся с наибольшими трудностями (возможно, потому что я так привык к тому, что Go парсит Yaml и JSON в заранее определенную структуру). Код для парсинга конфигурации в пользовательский тип оказался не таким уж красивым и очень многословным по нескольким причинам.
- Case-выражения растягивают код и приводят к большим отступам, но обеспечивают хороший способ обработки ошибок. И мне нравится, когда ошибки рассматриваются как значения.
- Вероятно, существует лучший пакет для парсинга Yaml в Gleam, но я не смог найти такового и использовал glaml, который я бы не назвал пригодным для производства (у него только 2 звезды на Github).
- В Gleam нет мутации, поэтому приходится часто использовать функции более высокого уровня, такие как
list.map
.
import glaml
import gleam/result
import simplifile
pub type Config {
Config(websites: List(Website))
}
pub type Website {
Website(url: String, interval: Int, pattern: String)
}
pub type ConfigError {
ConfigError(message: String)
}
pub fn load(filename: String) -> Result(Config, ConfigError) {
use file_data <- result.try(open_config_file(filename))
use websites <- result.try(parse_config_file(file_data))
Ok(Config(websites))
}
fn open_config_file(filename: String) -> Result(String, ConfigError) {
case simplifile.read(filename) {
Ok(data) -> Ok(data)
Error(_) -> Error(ConfigError(message: "Failed to read config file"))
}
}
// ...
Полный файл config.gleam находится здесь.
SQLite
Работать с SQLite оказалось проще простого. Я использовал пакет sqlight, и он хорошо справился со своей задачей. Возможно, функция exec()
должна принимать параметры, потому что мне пришлось использовать query()
для этого и инициализировать декодер, который на самом деле мне был не нужен, так как я не возвращал данные из базы, а только вставлял их.
import crawler
import gleam/bool
import gleam/dynamic
import sqlight
pub fn save_result(
db: sqlight.Connection,
result: crawler.CrawlResult,
) -> Result(Nil, sqlight.Error) {
// в этом нет необходимости, так как мы не считываем результат обратно
let mock_decoder = dynamic.tuple2(dynamic.int, dynamic.int)
case
sqlight.query(
"insert into websites (started_at, completed_at, status, pattern_matched, url) values (?, ?, ?, ?, ?)",
on: db,
with: [
sqlight.int(result.started_at),
sqlight.int(result.completed_at),
sqlight.int(result.status_code),
sqlight.int(result.pattern_matched |> bool.to_int),
sqlight.text(result.url),
],
expecting: mock_decoder,
)
{
Ok(_) -> Ok(Nil)
Error(e) -> Error(e)
}
}
Параллелизм и сборка
На этом этапе я написал парсер конфигурации, слой базы данных и HTTP-краулер. Пришло время собрать все вместе и запустить главный цикл. Я хотел создать параллельный процесс для каждого сайта и просматривать их с заданным интервалом.
В Gleam отлично реализован параллелизм — полагаю, он унаследовал его от Erlang. Я использовал пакет gleam/erlang
для создания связанного процесса.
// Запуск процесса для каждого сайта
list.each(c.websites, fn(w) {
process.start(fn() { process_website_recursively(db_conn, w) }, True)
})
fn process_website_recursively(
db_conn: sqlight.Connection,
website: config.Website,
) {
let result = crawler.crawl_url(website.url, website.pattern)
case database.save_result(db_conn, result) {
Ok(_) ->
io.println(string.append("Result saved successfully: ", website.url))
Error(e) -> io.println(string.append("Failed to save result: ", e.message))
}
process.sleep(website.interval * 1000)
process_website_recursively(db_conn, website)
}
Вы можете спросить, зачем использовать рекурсию? В Gleam нет циклов! Думаю, я смогу с этим смириться, если такая особенность будет компенсироваться неизменяемостью и отсутствием побочных эффектов.
Запуск в разных режимах выполнения
Я использовал gleam run
для локального запуска проекта в Erlang VM, и все прошло хорошо. Но позже я также хочу изучить возможности компиляции в JavaScript и запуска в браузере.
Тестирование
Я добавил простой модульный тест для парсера. Его было легко писать. Затем я использовал gleam test
для запуска тестов.
import config
import gleam/list
import gleeunit
import gleeunit/should
pub fn main() {
gleeunit.main()
}
pub fn parse_config_file_test() {
config.parse_config_file("invalid yaml data") |> should.be_error
let assert Ok(res) =
config.parse_config_file(
"websites:
- url: https://packagemain.tech
interval: 11",
)
let assert Ok(first) = res |> list.first
first.url |> should.equal("https://packagemain.tech")
first.interval |> should.equal(11)
}
Я хотел назвать файл по-другому, но не смог. Получается, у меня не может быть несколько тестовых файлов в Gleam?
Поддержка IDE
Я использую Zed, поэтому мне понадобилось просто интегрировать сервер языка Gleam. Все прошло гладко.
Заключение
Мне понравилось работать над проектом с Gleam. Больше всего пришлись мне по душе следующие особенности этого языка:
- неизменяемость и отсутствие побочных эффектов;
- конвейеры, например
first.url |> should.equal("https://packagemain.tech")
;
- тип
Result(value, error)
для обработки ошибок;
- параллельная обработка.
Изучение Gleam вдохновило меня на множество идей, и я с нетерпением жду, когда смогу написать больше проектов на этом языке.
Полный код вы можете найти в репозитории websites_checker.
Читайте также:
- LOESS в Rust
- Pascal: ностальгическое путешествие сквозь годы и код
- Введение в регулярные выражения в JavaScript
Читайте нас в Telegram, VK и Дзен
Перевод статьи Alex Pliutau: My first experience with Gleam Language