Basics


Hello world

Ето една мъничка програма, която отпечатва текста "Hello, Joe!". След малко ще обясним как работи.

В един нормален Gleam проект, тази програма ще се изпълни с командата gleam run от командния ред, но тук, в този урок, програмата се компилира и изпълнява във вашия уеб браузър, което ви позволява да опитате Gleam без да инсталирате нищо на вашия компютър.

Опитайте да промените текста, който се отпечатва на Hello, Mike! и вижте какво се случва.

import gleam/io

pub fn main() {
  io.println("Hello, Joe!")
}
</>Изпълняване на кода

Modules

Кодът в Gleam е организиран в единици, наречени модули. Един модул е съвкупност от дефиниции (на типове, функции и т.н.), които са свързани помежду си. Например, модулът gleam/io съдържа различни функции за печат, като println .

Всичкият Gleam код се намира в някакъв модул, чието име идва от името на файла, в който се намира. Например, кодът в gleam/io е във файл с име io.gleam в директория gleam.

За да достъпи код от един модул код от друг, ние го импортираме с ключовата дума import, а името, с което ще го наричаме, е последната част от името на модула. Например, след импортиране модулът gleam/io се нарича io.

Ключовата дума as може да се използва, за да наричаме един модул с друго име. Вижте как модулът gleam/string е наречен text тук.

import gleam/io
import gleam/string as text

pub fn main() {
  // Използване на функция от модула `gleam/io`
  io.println("Hello, Mike!")

  // Използване на функция от модула `gleam/string`
  io.println(text.reverse("Hello, Joe!"))
}
</>Изпълняване на кода

Unqualified imports

Обикновено функции от други модули се използват по квалифициран начин, което означава, че името, с което наричаме модула, стои преди името на функцията с точка между тях. Например, io.println("Hello!") .

Възможно е и да се посочи списък от функции, които да се импортират от един модул по неквалифициран начин, което означава, че името на функцията може да се използва без квалификатора на модула (името му и точката) преди него.

Обикновено е по-добре да се използват квалифицирани импорти, защото така става ясно къде е дефинирана функцията, което прави кода по-лесен за четене.

// Импортиране на модула и една от неговите функции
import gleam/io.{println}

pub fn main() {
  // Използване на функцията по квалифициран начин
  io.println("This is qualified")

  // Или по неквалифициран начин
  println("This is unqualified")
}
</>Изпълняване на кода

Type checking

Gleam има здрава система за статична проверка на типовете, която ви помага, докато пишете и редактирате код, като открива грешки и ви показва къде да направите промени.

Разокоментирайте реда io.println(4) и ще видите как се показва грешка по време на компилация, защото функцията io.println работи само със символни низове, а не с цели числа.

За да коригирате кода, променете го да извиква функцията io.debug , която ще отпечата стойност от всякакъв тип.

Gleam няма null, не прави неявни преобразувания на типове, няма изключения и винаги извършва пълна проверка на типовете. Ако кодът се компилира, можете да сте сравнително сигурни, че той няма несъответствия, които да причинят грешки или сривове.

import gleam/io

pub fn main() {
  io.println("My lucky number is:")
  // io.println(4)
  // 👆️ Разокоментирайте този ред
}
</>Изпълняване на кода

Ints

Типът Int в Gleam представлява цели числа.

Има аритметични и оператори за сравнение за цели числа, както и оператор за равенство, който работи с всички типове.

При изпълнение на виртуалната машина на Erlang целите числа нямат ограничение за минимален или максимален размер. При изпълнение в JavaScript среда, те се представят, използвайки 64-битови числа с плаваща запетая на JavaScript.

Стандартната библиотека gleam/int съдържа функции за работа с цели числа.

import gleam/int
import gleam/io

pub fn main() {
  // Аритметика с цели числа
  io.debug(1 + 1)
  io.debug(5 - 1)
  io.debug(5 / 2)
  io.debug(3 * 3)
  io.debug(5 % 2)

  // Сравнение на цели числа
  io.debug(2 > 1)
  io.debug(2 < 1)
  io.debug(2 >= 1)
  io.debug(2 <= 1)

  // Равенството работи за всички типове
  io.debug(1 == 1)
  io.debug(2 == 1)

  // Функции от стандартната библиотека за цели числа
  io.debug(int.max(42, 77))
  io.debug(int.clamp(5, 10, 20))
}
</>Изпълняване на кода

Floats

Типът Float в Gleam представлява числа с плаваща запетая.

Числовите оператори в Gleam не са претоварени, затова има специални оператори за работа с числа с плаваща запетая.

Числата с плаваща запетая се представят като 64-битови числа с плаваща запетая и в Erlang, и в JavaScript средата. Поведението им е това на съответната среда, затова ще има малки разлики в поведението им в двете среди.

В JavaScript средата, надвишаването на максималната (или минималната) представима стойност за число с плаваща запетая ще доведе до Infinity (или -Infinity). Ако се опитате да разделите две безкрайности, ще получите NaN като резултат.

При изпълнение на BEAM всяко препълване ще генерира грешка. Затова в Erlang средата няма NaN или Infinity за числата с плаваща запетая.

Деленето на нула няма да препълни, а е дефинирано да дава нула.

Стандартната библиотека gleam/float съдържа функции за работа с числа с плаваща запетая.

import gleam/float
import gleam/io

pub fn main() {
  // Аритметика с числа с плаваща запетая
  io.debug(1.0 +. 1.5)
  io.debug(5.0 -. 1.5)
  io.debug(5.0 /. 2.5)
  io.debug(3.0 *. 3.5)

  // Сравнение на числа с плаваща запетая
  io.debug(2.2 >. 1.3)
  io.debug(2.2 <. 1.3)
  io.debug(2.2 >=. 1.3)
  io.debug(2.2 <=. 1.3)

  // Равенството работи за всички типове
  io.debug(1.1 == 1.1)
  io.debug(2.1 == 1.2)

  // Деленето на нула не е грешка
  io.debug(3.14 /. 0.0)

  // Функции от стандартната библиотека за числа с плаваща запетая
  io.debug(float.max(2.0, 9.5))
  io.debug(float.ceiling(5.4))
}
</>Изпълняване на кода

Number formats

Долната черта може да се добавя в числа за по-добра четливост. Например, 1000000 е трудно за бързо четене, докато 1_000_000 е по-лесно.

Целите числа могат да се пишат в двоичен, осмичен или шестнадесетичен формат с помощта на префиксите 0b, 0o и 0x, съответно.

Числата с плаваща запетая могат да се пишат в научна нотация.

import gleam/io

pub fn main() {
  // Долни черти
  io.debug(1_000_000)
  io.debug(10_000.01)

  // Литерали за цели числа в двоичен, осмичен и шестнадесетичен формат
  io.debug(0b00001111)
  io.debug(0o17)
  io.debug(0xF)

  // Литерали за числа с плаваща запетая в научна нотация
  io.debug(7.0e7)
  io.debug(3.0e-4)
}
</>Изпълняване на кода

Equality

Gleam има операторите == and != за проверка на равенство.

Операторите могат да се използват със стойности от всякакъв тип, но двата операнда трябва да са от един и същи тип.

Равенството се проверява структурно, което означава, че две стойности са равни, ако имат една и съща структура, а не ако са на едно и също място в паметта.

import gleam/io

pub fn main() {
  io.debug(100 == 100)
  io.debug(1.5 != 0.1)
}
</>Изпълняване на кода

Strings

В Gleam символните низове се пишат като текст, ограден с двойни кавички, и могат да се простират на няколко реда и да съдържат Unicode символи.

Операторът <> се използва за конкатенация на символни низове.

Поддържат се няколко екраниращи последователности:

  • \" - двойни кавички
  • \\ - обратна наклонена черта
  • \f - нов формуляр
  • \n - нов ред
  • \r - връщане в началото на реда
  • \t - табулация
  • \u{xxxxxx} - Unicode кодова точка

Стандартната библиотека gleam/string съдържа функции за работа със символни низове.

import gleam/io
import gleam/string

pub fn main() {
  // Литерали за символни низове
  io.debug("👩‍💻 こんにちは Gleam 🏳️‍🌈")
  io.debug(
    "multi
    line
    string",
  )
  io.debug("\u{1F600}")

  // Двойните кавички могат да се екранират
  io.println("\"X\" marks the spot")

  // Конкатенация на символни низове
  io.debug("One " <> "Two")

  // Функции за символни низове
  io.debug(string.reverse("1 2 3 4 5"))
  io.debug(string.append("abc", "def"))
}
</>Изпълняване на кода

Bools

Bool е или True (вярно), или False (невярно).

Операторите || (логическо ИЛИ), && (логическо И) и ! (логическо НЕ) се използват за работа с булеви стойности.

Операторите || и && са с късо съединение, което означава, че ако лявата страна на оператора е True за || или False за &&, дясната страна на оператора няма да бъде оценена.

Стандартната библиотека gleam/bool съдържа функции за работа с булеви стойности.

import gleam/bool
import gleam/io

pub fn main() {
  // Оператори за булеви стойности
  io.debug(True && False)
  io.debug(True && True)
  io.debug(False || False)
  io.debug(False || True)

  // Функции за булеви стойности
  io.debug(bool.to_string(True))
  io.debug(bool.to_int(False))
}
</>Изпълняване на кода

Assignments

Стойност може да се присвои на променлива, използвайки let.

Имената на променливи могат да се използват повторно в следващи let връзки, но стойностите, към които се отнасят, са неизменни, затова самите те не се променят по никакъв начин.

В Gleam, имената на променливи и функции се пишат в стил snake_case (с долна черта между думите).

import gleam/io

pub fn main() {
  let x = "Original"
  io.debug(x)

  // Присвояване на стойността на `x` на `y`
  let y = x
  io.debug(y)

  // Присвояване на нова стойност на `x`
  let x = "New"
  io.debug(x)

  // `y` все още сочи към първоначалната стойност
  io.debug(y)
}
</>Изпълняване на кода

Discard patterns

Ако една променлива е декларирана, но не се използва, Gleam ще покаже предупреждение.

Ако не е предвидено да се използва променлива, името ѝ може да започне с долна черта, което ще заглуши предупреждението.

Опитайте да смените името на променливата на score, за да видите предупреждението.

pub fn main() {
  // Тази променлива не се използва никога
  let _score = 1000
}
</>Изпълняване на кода

Type annotations

Присвояванията с let могат да се пишат с анотация на типа след името.

Анотациите на типовете може да са полезни за документиране, но не променят как Gleam проверява типовете в кода, освен за да се гарантира, че анотацията е коректна.

Обикновено кодът в Gleam няма анотации на типовете при присвоявания.

Опитайте да промените една от анотациите на типове на нещо некоректно, за да видите грешка при компилация.

pub fn main() {
  let _name: String = "Gleam"

  let _is_cool: Bool = True

  let _version: Int = 1
}
</>Изпълняване на кода

Type imports

Други модули може да дефинират типове, които искаме да използваме. В този случай е нужно да ги импортираме.

Както при функциите, към типовете може да се обръщаме по квалифициран начин, като слагаме името на импортирания модул и точка преди името на типа. Например, bytes_builder.BytesBuilder

Типовете може да се импортират и по неквалифициран начин, като се изброят в израза за импортиране с ключовата дума type пред името им.

За разлика от функциите, в Gleam типовете обикновено се импортират по неквалифициран начин.

import gleam/bytes_builder
import gleam/string_builder.{type StringBuilder}

pub fn main() {
  // Обръщане към тип по квалифициран начин
  let _bytes: bytes_builder.BytesBuilder = bytes_builder.new()

  // Обръщане към тип по неквалифициран начин
  let _text: StringBuilder = string_builder.new()
}
</>Изпълняване на кода

Type aliases

Алиас на тип може да се използва, за да се обръщаме към тип с друго име. Даването на алиас на един тип не създава нов тип, все още става дума за същия тип.

Имената на типовете винаги започват с главна буква, за разлика от променливите и функциите, които започват с малка буква.

Когато се използва ключовата дума pub, алиасът на типа е публичен и може да се използва и от други модули.

import gleam/io

pub type UserId =
  Int

pub fn main() {
  let one: UserId = 1
  let two: Int = 2

  // UserId и Int са един и същи тип
  io.debug(one == two)
}
</>Изпълняване на кода

Blocks

Блоковете са един или повече изрази, групирани с къдрави скоби. Всеки израз се оценява по ред и стойността на последния израз е връщаната стойност на блока.

Всяка променлива, декларирана в блока, може да се използва само в блока.

Опитайте да разокоментирате io.debug(degrees) , за да видите грешка при компилация заради опита да се използва променлива, която е извън обхвата си.

Блоковете може да се използват и за промяна на реда на оценяване в изрази с бинарни оператори.

* е с по-висок приоритет от +, затова изразът 1 + 2 * 3 се оценява на 7. Ако искаме първо да се оцени 1 + 2, за да е 9 стойността на целия израз, е нужно да го сложим в блок: { 1 + 2 } * 3. Това е подобно на групирането със скоби в някои други езици.

import gleam/io

pub fn main() {
  let fahrenheit = {
    let degrees = 64
    degrees
  }
  // io.debug(degrees) // <- Това няма да се компилира

  // Промяна на реда на оценяване
  let celsius = { fahrenheit - 32 } * 5 / 9
  io.debug(celsius)
}
</>Изпълняване на кода

Lists

Списъците са наредени колекции от стойности.

List е обобщен тип (generic type), който има тип-параметър за типа на стойностите, които съдържа. Списък от цели числа е от тип List(Int), а списък от символни низове - от тип List(String).

Списъците в Gleam са неизменни (immutable) едносвързани списъци, което означава, че е много ефективно да се добавят и премахват елементи от началото на списъка.

Преброяването на елементите в списък или вземането на елементи от други позиции в списъка е бавно и рядко се прави. В Gleam рядко се пишат алгоритми, които индексират поредици, но когато това се налага, списъкът не е правилният избор за структура от данни.

import gleam/io

pub fn main() {
  let ints = [1, 2, 3]

  io.debug(ints)

  // Добавяне на елементи отпред
  io.debug([-1, 0, ..ints])

  // Разокоментирайте, за да видите грешка
  // io.debug(["zero", ..ints])

  // Оригиналните списъци не се променят
  io.debug(ints)
}
</>Изпълняване на кода

Constants

Освен присвояване с let, Gleam има и константи, които се дефинират на най-високо ниво в модула.

Константите трябва да са литерални стойности, не може да се използват функции в дефинициите им.

Константите може да са полезни за стойности, които се използват многократно в програмата ви, позволявайки им да бъдат именувани, както и гарантирайки, че дефиницията е една и съща навсякъде, където се използват.

Използването на константа може да е по-ефективно от създаването на една и съща стойност в множество функции, макар че производителността ще зависи от средата за изпълнение и дали се компилира за Erlang или JavaScript.

import gleam/io

const ints: List(Int) = [1, 2, 3]

const floats = [1.0, 2.0, 3.0]

pub fn main() {
  io.debug(ints)
  io.debug(ints == [1, 2, 3])

  io.debug(floats)
  io.debug(floats == [1.0, 2.0, 3.0])
}
</>Изпълняване на кода

Functions


Functions

Ключовата дума fn се използва за дефиниране на нови функции.

Функциите double (удвояване) и multiply (умножаване) са дефинирани без ключовата дума pub. Това ги прави частни (private) функции, те може да се използват само в този модул. Ако друг модул се опита да ги използва, ще има грешка при компилация.

Както при присвояването, анотациите на типовете са по желание за аргументите на функциите и за връщаната от тях стойност. Счита се за добра практика да се използват анотации на типовете за функциите за по-добра яснота и за насърчаване на внимателно проектиране.

import gleam/io

pub fn main() {
  io.debug(double(10))
}

fn double(a: Int) -> Int {
  multiply(a, 2)
}

fn multiply(a: Int, b: Int) -> Int {
  a * b
}
</>Изпълняване на кода

Higher order functions

В Gleam функциите са стойности. Те може да се присвояват на променливи, да се подават като аргументи на други функции и всичко, което може да се прави със стойности.

Тук функцията add_one (добавяне на 1) се подава като аргумент на функцията twice (два пъти).

Забележете, че ключовата дума fn се използва и за описание на типа на функцията, която twice приема за втория си аргумент.

import gleam/io

pub fn main() {
  // Извикване на функция с друга функция като аргумент
  io.debug(twice(1, add_one))

  // Функции може да се присвояват на променливи
  let my_function = add_one
  io.debug(my_function(100))
}

fn twice(argument: Int, passed_function: fn(Int) -> Int) -> Int {
  passed_function(passed_function(argument))
}

fn add_one(argument: Int) -> Int {
  argument + 1
}
</>Изпълняване на кода

Anonymous functions

Освен именувани функции на ниво модул, Gleam поддържа и анонимни функции, дефинирани чрез синтаксиса fn() { ... }.

Анонимните функции може да се ползват напълно заместимо с именувани функции.

Анонимните функции може да реферират променливи, които са били в обхват при дефинирането им, което ги прави затваряния (closures).

import gleam/io

pub fn main() {
  // Присвояване на анонимна функция на променлива
  let add_one = fn(a) { a + 1 }
  io.debug(twice(1, add_one))

  // Подаване на анонимна функция като аргумент
  io.debug(twice(1, fn(a) { a * 2 }))

  let secret_number = 42
  // Тази анонимна функция винаги връща 42
  let secret = fn() { secret_number }
  io.debug(secret())
}

fn twice(argument: Int, my_function: fn(Int) -> Int) -> Int {
  my_function(my_function(argument))
}
</>Изпълняване на кода

Function captures

Gleam има съкратен синтаксис за създаване на анонимни функции, които приемат един аргумент и веднага извикват друга функция с този аргумент: синтаксисът за улавяне на функции (function capture syntax).

Анонимната функция fn(a) { some_function(..., a, ...) } може да се напише като some_function(..., _, ...), като се подават още произволен брой аргументи директно на вътрешната функция. Долната черта _ е заместител (placeholder) на аргумента, еквивалентен на a в fn(a) { some_function(..., a, ...) }.

import gleam/io

pub fn main() {
  // Тези две декларации са еквивалентни
  let add_one_v1 = fn(x) { add(1, x) }
  let add_one_v2 = add(1, _)

  io.debug(add_one_v1(10))
  io.debug(add_one_v2(10))
}

fn add(a: Int, b: Int) -> Int {
  a + b
}
</>Изпълняване на кода

Generic functions

Дотук всяка функция приемаше точно един тип за всеки от аргументите си.

Функцията twice (два пъти) от предния урок за функции от по-висок ред работеше само с функции, които приемат и връщат цели числа. Това е прекалено ограничаващо, би трябвало да можем да използваме тази функция с всякакви типове, стига подаваната функция и първоначалната стойност да са съвместими.

За да стане това, Gleam поддържа обобщени функции (generics), известни още като параметричен полиморфизъм.

Това се прави като се използва променлива за тип, вместо да се указва конкретен тип. Тя е заместител (placeholder) на какъвто специфичен тип се използва при извикване на функцията. Тези променливи за тип се пишат с имена с малки букви.

Променливите за тип не са като тип any, те се заместват с конкретен тип при всяко извикване на функцията. Опитайте да разокоментирате twice(10, exclaim), за да видите грешка при компилация, защото се опитваме да използваме променлива за тип и за цяло число, и за символен низ едновременно.

import gleam/io

pub fn main() {
  let add_one = fn(x) { x + 1 }
  let exclaim = fn(x) { x <> "!" }

  // Невалидно, Int и String не са съвместими типове
  // twice(10, exclaim)

  // Тук променливата за тип е заменена с типа Int
  io.debug(twice(10, add_one))

  // Тук променливата за тип е заменена с типа String
  io.debug(twice("Hello", exclaim))
}

// Името `value` (стойност) реферира към един и същи тип на няколко места
fn twice(argument: value, my_function: fn(value) -> value) -> value {
  my_function(my_function(argument))
}
</>Изпълняване на кода

Pipelines

Често се налага да викаме няколко функции, подавайки резултата от едната като аргумент на следващата. С нормалния синтаксис за извикване на функции това е трудно за четене, защото е нужно да се чете кода отвътре навън.

Операторът за конвейери (pipelines) |> в Gleam ни помага в такива ситуации, като ни позволява да пишем кода отгоре надолу.

Операторът |> приема резултата от израза от лявата си страна и го подава като аргумент на функцията от дясната си страна.

Първо се проверява дали стойността отляво може да се използва като първи аргумент на извикването. Например, a |> b(1, 2) би се превърнало в b(a, 1, 2). Ако това е невъзможно, резултата от дясната страна се извиква като функция: b(1, 2)(a).

Обикновено Gleam кодът се пише със "субекта" (subject) на функцията като първи аргумент, за да се улеснят конвейерите. Ако искате да използвате конвейер, подавайки стойност като непърви аргумент, може да се използва улавяне на функция (function capture), за да се вмъкне стойността на желаната позиция.

import gleam/io
import gleam/string

pub fn main() {
  // Без оператора за конвейер
  io.debug(string.drop_left(string.drop_right("Hello, Joe!", 1), 7))

  // С оператора за конвейер
  "Hello, Mike!"
  |> string.drop_right(1)
  |> string.drop_left(7)
  |> io.debug

  // Промяна на реда с улавяне на функция
  "1"
  |> string.append("2")
  |> string.append("3", _)
  |> io.debug
}
</>Изпълняване на кода

Labelled arguments

Когато функциите приемат няколко аргумента, е трудно да запомним какви са те и в какъв ред се очакват.

За да ни помогне, Gleam поддържа именувани аргументи (labelled arguments), при които аргументите на функциите получават етикет (label), освен вътрешното си име. Тези етикети се пишат преди името на аргумента в дефиницията на функцията.

Когато се използват именувани аргументи, редът им няма значение, но всички аргументи без етикети трябва да са преди именуваните аргументи.

Няма разлика в производителността при използване на именувани аргументи, не се създава речник, нито се извършва друга работа по време на изпълнение.

Етикетите не са задължителни, когато викаме функция, програмистът сам решава какво е по-ясно в кода му.

import gleam/io

pub fn main() {
  // Без етикети
  io.debug(calculate(1, 2, 3))

  // С етикети
  io.debug(calculate(1, add: 2, multiply: 3))

  // С етикети в различен ред
  io.debug(calculate(1, multiply: 3, add: 2))
}

fn calculate(value: Int, add addend: Int, multiply multiplier: Int) {
  value * multiplier + addend
}
</>Изпълняване на кода

Documentation comments

Документацията и коментарите са важни, за да бъде кодът ни по-разбираем и по-лесен за работа.

Освен обикновени коментари с //, Gleam има и /// и //// коментари, които служат за добавяне на документация към кода.

/// се използва за документиране на типове и функции и трябва да стои непосредствено преди типа или функцията, които документира.

//// се използва за документиране на модули и трябва да стои в началото на модула.

//// Модул, съдържащ някои необичайни функции и типове.

/// Тип, чиято стойност не може да се конструира.
/// Можете ли да се досетите защо?
pub type Never {
  Never(Never)
}

/// Извиква функция два пъти с първоначална стойност.
///
pub fn twice(argument: value, my_function: fn(value) -> value) -> value {
  my_function(my_function(argument))
}

/// Извиква функция три пъти с първоначална стойност.
///
pub fn thrice(argument: value, my_function: fn(value) -> value) -> value {
  my_function(my_function(my_function(argument)))
}
</>Изпълняване на кода

Deprecations

Функции и други дефиниции може да се маркират като deprecated (непрепоръчителни за използване), използвайки атрибута @deprecated.

Ако се реферира към deprecated функция, компилаторът ще покаже предупреждение, за да се уведоми програмиста, че е добре да промени кода си.

Атрибутът @deprecated приема съобщение, което ще се покаже на потребителя в предупреждението. В съобщението се обяснява новият подход или функцията, която трябва да се използва вместо нея, или се дава линк към документацията за миграция.

pub fn main() {
  old_function()
  new_function()
}

@deprecated("Use new_function instead")
fn old_function() {
  Nil
}

fn new_function() {
  Nil
}
</>Изпълняване на кода

Flow control


Case expressions

case изразите са най-често срещаният вид управление на потока (flow control) в Gleam кода. Те са подобни на switch в някои други езици, но са по-мощни.

case изразът позволява на програмиста да каже "ако данните имат този вид, изпълни този код", процес, наречен съпоставяне с шаблони (pattern matching).

Gleam извършва проверка за изчерпателност (exhaustiveness checking), за да е сигурно, че шаблоните в case израза покриват всички възможни стойности. По този начин можете да сте уверени, че логиката ви е актуална според типа на данните, с които работите.

Опитайте да коментирате някои шаблони или да добавите излишни и ще видите какви проблеми ще открие компилаторът.

import gleam/int
import gleam/io

pub fn main() {
  let x = int.random(5)
  io.debug(x)

  let result = case x {
    // Съпоставяне на конкретни стойности
    0 -> "Zero"
    1 -> "One"

    // Съпоставяне на всички други стойности
    _ -> "Other"
  }
  io.debug(result)
}
</>Изпълняване на кода

Variable patterns

Шаблоните в case изразите може да декларират променливи.

Когато име на променлива се използва в шаблон, стойността, която съвпада с този шаблон, се присвоява на променливата, която може да се използва в тялото на съответния case клон.

import gleam/int
import gleam/io

pub fn main() {
  let result = case int.random(5) {
    // Съпоставяне на конкретни стойности
    0 -> "Zero"
    1 -> "One"

    // Съпоставяне на всички други стойности
    // и присвояването им на променливата `other`
    other -> "It is " <> int.to_string(other)
  }
  io.debug(result)
}
</>Изпълняване на кода

String patterns

Когато съпоставяме със символни низове, операторът <> може да се използва за съвпадение със символни низове, започващи с определен префикс.

Шаблонът "Hello, " <> name съвпада с всеки символен низ, който започва с "Hello, " и присвоява остатъка от низа на променливата name (име).

import gleam/io

pub fn main() {
  io.debug(get_name("Hello, Joe"))
  io.debug(get_name("Hello, Mike"))
  io.debug(get_name("System still working?"))
}

fn get_name(x: String) -> String {
  case x {
    "Hello, " <> name -> name
    _ -> "Unknown"
  }
}
</>Изпълняване на кода

List patterns

Списъците, както и елементите, които съдържат, може да участват в съпоставяне с шаблони (pattern matching) в case изрази.

Шаблоните за списък съвпадат със списъци с определена дължина. Шаблонът [] съвпада с празен списък, а шаблонът [_] - със списък с един елемент. Те няма да съвпадат със списъци с друга дължина.

Шаблонът с разгъване .. може да се използва за съвпадение с остатъка от списъка. Шаблонът [1, ..] съвпада с всеки списък, започващ с 1. Шаблонът [_, _, ..] съвпада с всеки списък с поне два елемента.

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

pub fn main() {
  let x = list.repeat(int.random(5), times: int.random(3))
  io.debug(x)

  let result = case x {
    [] -> "Empty list"
    [1] -> "List of just 1"
    [4, ..] -> "List starting with 4"
    [_, _] -> "List of 2 elements"
    _ -> "Some other list"
  }
  io.debug(result)
}
</>Изпълняване на кода

Recursion

Gleam няма цикли (loops), вместо това итерацията се прави чрез рекурсия, т.е. чрез функции на най-високо ниво, които извикват самите себе си с различни аргументи.

Всяка рекурсивна функция трябва да има поне един базов случай (base case) и поне един рекурсивен случай (recursive case). Базовият случай връща стойност, без да извиква функцията отново. Рекурсивният случай извиква функцията отново с различни аргументи, което повтаря цикъла.

Стандартната библиотека на Gleam има функции за различни често срещани цикли, някои от които ще се разгледат в следващите уроци, но за по-сложни цикли рекурсията често е най-лесният за четене начин да се напишат.

Рекурсията може да ви изглежда обезкуражаваща, ако сте свикнали с езици, които имат специални инструменти за итерация, но не се отказвайте! С времето ще ви стане толкова позната, колкото и всеки друг начин за итерация.

import gleam/io

pub fn main() {
  io.debug(factorial(5))
  io.debug(factorial(7))
}

// Рекурсивна функция за пресмятане на факториел
pub fn factorial(x: Int) -> Int {
  case x {
    // Базов случай
    0 -> 1
    1 -> 1

    // Рекурсивен случай
    _ -> x * factorial(x - 1)
  }
}
</>Изпълняване на кода

Tail calls

Когато се извика функция, се създава нов кадър (stack frame) в паметта, за да се запазят аргументите и локалните променливи на функцията. Ако се създават много такива кадри по време на рекурсия, програмата би използвала много памет или дори може да се срине, ако достигне ограничение за памет.

За да избегнем този проблем, Gleam поддържа оптимизация за опашково извикване (tail call optimisation), което позволява на компилатора да използва повторно кадъра на текущата функция, ако последното нещо, което функцията прави, е да извика себе си.

Неоптимизирани рекурсивни функции често може да се пренапишат така, че да станат оптимизирани за опашково извикване чрез използване на акумулатор. Акумулаторът е променлива, която се подава заедно с данните, подобно на променлива, чиято стойност се променя в езици с цикли while.

Акумулаторите трябва да се крият от потребителите на вашия код, те са детайли от имплементацията. За целта дефинирайте публична функция, която извиква частна рекурсивна функция с първоначалната стойност на акумулатора.

import gleam/io

pub fn main() {
  io.debug(factorial(5))
  io.debug(factorial(7))
}

pub fn factorial(x: Int) -> Int {
  // Публичната функция извиква частната опашково-рекурсивна функция
  factorial_loop(x, 1)
}

fn factorial_loop(x: Int, accumulator: Int) -> Int {
  case x {
    0 -> accumulator
    1 -> accumulator

    // Последното нещо, което прави тази функция, е да извика себе си.
    // В предишния урок последното нещо, което правеше беше да умножи две цели числа.
    _ -> factorial_loop(x - 1, accumulator * x)
  }
}
</>Изпълняване на кода

List recursion

Макар да е по-обичайно да се използват функции от модула gleam/list , за да се обходят елементите на списък, понякога може да е по-добре да работите директно със списъка.

Шаблонът [first, ..rest] съвпада със списък с поне един елемент, присвоявайки първия елемент на променливата first, а останалите - на променливата rest. Използвайки този шаблон заедно с шаблона за празен списък [], може да напишем функция, която обработва елементите на списъка, докато стигне до края му.

Този код сумира елементите на списък, като рекурсивно го обхожда и добавя всяко число към total (сума), връщайки я при достигане на края му.

import gleam/io

pub fn main() {
  let sum = sum_list([18, 56, 35, 85, 91], 0)
  io.debug(sum)
}

fn sum_list(list: List(Int), total: Int) -> Int {
  case list {
    [first, ..rest] -> sum_list(rest, total + first)
    [] -> total
  }
}
</>Изпълняване на кода

Multiple subjects

Понякога се налага да се съпостави с шаблон едновременно на множество стойности в един и същи case израз.

За да направим това, може да подадем няколко "субекта" (subjects) и няколко шаблона, разделени със запетаи.

Когато съпоставяме с множество "субекта", е нужно броят на шаблоните да е равен на броя "субекта". Опитайте да премахнете един от _, шаблоните, за да видите каква грешка при компилация се появява.

import gleam/int
import gleam/io

pub fn main() {
  let x = int.random(2)
  let y = int.random(2)
  io.debug(x)
  io.debug(y)

  let result = case x, y {
    0, 0 -> "Both are zero"
    0, _ -> "First is zero"
    _, 0 -> "Second is zero"
    _, _ -> "Neither are zero"
  }
  io.debug(result)
}
</>Изпълняване на кода

Alternative patterns

Във всеки клон (clause) на case израза може да се зададат алтернативни шаблони (patterns) с помощта на оператора | (вертикална черта). Ако някой от шаблоните съвпадне, клонът е изпълнен.

Ако даден шаблон декларира променлива, всички алтернативни шаблони в този клон е нужно да декларират променлива със същото име и тип.

В момента не се поддържа влагане на алтернативни шаблони, затова шаблонът [1 | 2 | 3] не е валиден.

import gleam/int
import gleam/io

pub fn main() {
  let number = int.random(10)
  io.debug(number)

  let result = case number {
    2 | 4 | 6 | 8 -> "This is an even number"
    1 | 3 | 5 | 7 -> "This is an odd number"
    _ -> "I'm not sure"
  }
  io.debug(result)
}
</>Изпълняване на кода

Pattern aliases

Операторът as може да се използва за присвояване на променливи на подшаблони.

Шаблонът [_, ..] as first съвпада с всеки непразен списък и го присвоява на променливата first (първи).

import gleam/io

pub fn main() {
  io.debug(get_first_non_empty([[], [1, 2, 3], [4, 5]]))
  io.debug(get_first_non_empty([[1, 2], [3, 4, 5], []]))
  io.debug(get_first_non_empty([[], [], []]))
}

fn get_first_non_empty(lists: List(List(t))) -> List(t) {
  case lists {
    [[_, ..] as first, ..] -> first
    [_, ..rest] -> get_first_non_empty(rest)
    [] -> []
  }
}
</>Изпълняване на кода

Guards

Ключовата дума if може да се използва в case изрази, за да добавим guard (охранител/защита?) към даден шаблон. Guard е израз, който трябва да се оцени на True (вярно), за да може шаблонът да съвпадне.

В guard-ове може да се използва само ограничено множество оператори и е невъзможно да се извикват функции.

import gleam/io

pub fn main() {
  let numbers = [1, 2, 3, 4, 5]
  io.debug(get_first_larger(numbers, 3))
  io.debug(get_first_larger(numbers, 5))
}

fn get_first_larger(numbers: List(Int), limit: Int) -> Int {
  case numbers {
    [first, ..] if first > limit -> first
    [_, ..rest] -> get_first_larger(rest, limit)
    [] -> 0
  }
}
</>Изпълняване на кода

Data types


Tuples

Списъците са подходящи, когато имаме колекция от елементи от един тип, но понякога се налага да комбинираме няколко стойности от различни типове. В този случай n-торките (tuples) са бърз и удобен вариант.

Синтаксисът за достъп до елементи на n-торки ни позволява да вземем отделните им елементи без съпоставяне с шаблон. some_tuple.0 (some_tuple - някаква n-торка) взима първия елемент, some_tuple.1 взима втория и т.н.

N-торките са обобщени типове (generics), те имат тип параметри за типовете на елементите си. #(1, "Hi!") // Hi! - Здрасти! е от тип #(Int, String), a #(1.4, 10, 48) е от тип #(Float, Int, Int).

N-торките се използват най-често, за да връщат 2 или 3 стойности от дадена функция. Често е по-ясно да се използва потребителски тип (custom type) там, където е възможно да се ползва n-торка. Ще разгледаме потребителски типове в следващия урок.

import gleam/io

pub fn main() {
  let triple = #(1, 2.2, "three")
  io.debug(triple)

  let #(a, _, _) = triple
  io.debug(a)
  io.debug(triple.1)
}
</>Изпълняване на кода

Custom types

Gleam има няколко вградени типа (built-in types), например, Int и String, но потребителските типове (custom types) позволяват създаване на съвсем нови типове.

Потребителски тип се дефинира с ключовата дума type, следвана от името на типа и по един конструктор (constructor) за всеки вариант (variant) на типа. И името на типа, и имената на конструкторите започват с главна буква.

Възможно е да правим съпоставяне с шаблон (pattern matching) с case израз върху вариантите на потребителски типове.

import gleam/io

pub type Season {
  Spring
  Summer
  Autumn
  Winter
}

pub fn main() {
  io.debug(weather(Spring))
  io.debug(weather(Autumn))
}

fn weather(season: Season) -> String {
  case season {
    Spring -> "Mild"
    Summer -> "Hot"
    Autumn -> "Windy"
    Winter -> "Cold"
  }
}
</>Изпълняване на кода

Records

Всеки вариант (variant) на потребителски тип може да пази допълнителни данни в себе си. В този случай вариантът се нарича record.

Полетата на record може да получат етикети (labels), подобно на аргументите на функция, и е по желание да се използват при извикване на конструктора. Обикновено етикетите се използват за варианти, които ги дефинират.

Често се дефинират потребителски типове само с един вариант, който пази данни. Това е еквивалентът в Gleam на struct или object от други езици за програмиране.

import gleam/io

pub type SchoolPerson {
  Teacher(name: String, subject: String)
  Student(String)
}

pub fn main() {
  let teacher1 = Teacher("Mr Schofield", "Physics")
  let teacher2 = Teacher(name: "Miss Percy", subject: "Physics")
  let student1 = Student("Koushiar")
  let student2 = Student("Naomi")
  let student3 = Student("Shaheer")

  let school = [teacher1, teacher2, student1, student2, student3]
  io.debug(school)
}
</>Изпълняване на кода

Record accessors

Синтаксисът record.field_label (record - запис, field_label - етикет на поле) може да се използва, за да вземем стойност от поле на потребителски тип, който е record.

Синтаксисът record.field_label може да се използва само за полета с едно и също име, които са на една и съща позиция и са от един и същи тип във всички варианти на потребителския тип.

Полето name (име) е на първа позиция и е от тип String във всички варианти, затова можем да достъпваме record.name.

Полето subject (предмет) липсва във варианта Student, затова record.subject не може да се използва с никой вариант от тип SchoolPerson (човек в училище). Разокоментирайте реда с teacher.subject (teacher - учител), за да видите грешката при компилация от опит да се използва този accessor.

import gleam/io

pub type SchoolPerson {
  Teacher(name: String, subject: String)
  Student(name: String)
}

pub fn main() {
  let teacher = Teacher("Mr Schofield", "Physics")
  let student = Student("Koushiar")

  io.debug(teacher.name)
  io.debug(student.name)
  // io.debug(teacher.subject)
}
</>Изпълняване на кода

Record pattern matching

Възможно е да правим съпоставяне с шаблони (pattern matching) върху record. Това ни позволява да извлечем стойностите на няколко полета от record в отделни променливи, подобно на съпоставяне с шаблони за n-торки или списъци.

Ключовата дума let може да съвпада само с потребителски типове, които имат само един вариант. За типове с повече варианти, е нужно да се използва case израз.

Възможно е да се използват долната черта _ или шаблонът с разгъване .., за да се игнорират полета, които не са ни нужни.

import gleam/io

pub type Fish {
  Starfish(name: String, favourite_color: String)
  Jellyfish(name: String, jiggly: Bool)
}

pub fn main() {
  let lucy = Starfish("Lucy", "Pink")

  case lucy {
    Starfish(_, favourite_color) -> io.debug(favourite_color)
    Jellyfish(name, ..) -> io.debug(name)
  }
}
</>Изпълняване на кода

Record updates

Синтаксисът за актуализиране на record може да се използва, за да се създаде нов record, базиран на вече съществуващ от същия тип, но с променени стойности на някои от полетата му.

Gleam е език с неизменни (immutable) стойности, затова синтаксисът за актуализация на record не променя оригиналния record по никакъв начин.

import gleam/io

pub type SchoolPerson {
  Teacher(name: String, subject: String, floor: Int, room: Int)
}

pub fn main() {
  let teacher1 = Teacher(name: "Mr Dodd", subject: "ICT", floor: 2, room: 2)

  // Използване на синтаксис за актуализация
  let teacher2 = Teacher(..teacher1, subject: "PE", room: 6)

  io.debug(teacher1)
  io.debug(teacher2)
}
</>Изпълняване на кода

Generic custom types

Както функциите, потребителските типове (custom types) може да са обобщени (generic), т.е. да приемат типове като параметри.

Тук е дефиниран обобщен тип Option (опция), който се използва, за да представи стойност, която или я има, или я няма. Този тип е много полезен! Модулът gleam/option го дефинира, така че може да го използвате в Gleam проекти.

pub type Option(inner) {
  Some(inner)
  None
}

// Опция от символен низ
pub const name: Option(String) = Some("Annah")

//Опция от цяло число
pub const level: Option(Int) = Some(10)
</>Изпълняване на кода

Nil

Nil (нула, празно) е единичен (unit) тип в Gleam. Това е стойността, която връщат функциите, които нямат друга стойност за връщане, тъй като всички функции в Gleam е задължително да връщат нещо.

Nil не е валидна стойност за никой друг тип. Затова стойностите в Gleam не може да са null (нула, празно). Ако дадена стойност е от тип Nil, значи стойността ѝ е Nil. Ако е от някакъв друг тип, значи стойността ѝ не е Nil.

Разокоментирайте реда, в който се присвоява Nil на променлива, чийто тип не е съвместим с него, за да видите грешка при компилация.

import gleam/io

pub fn main() {
  let x = Nil
  io.debug(x)

  // let y: List(String) = Nil

  let result = io.println("Hello!")
  io.debug(result == Nil)
}
</>Изпълняване на кода

Results

Gleam не използва изключения (exceptions), вместо това пресмятанията (computations), които може да са успешни (succeed) или да се провалят (fail), връщат стойност от вградения тип Result(value, error) (резултат - стойност, грешка). Този тип има два варианта (variants):

  • Ok (добре, успешно), който съдържа връщаната стойност (return value) при успех на пресмятането.
  • Error (грешка), който съдържа причината за неуспеха на пресмятането.

Типът е обобщен (generic) с два тип-параметъра - един за стойността при успех и един за грешката. По този начин Result може да съдържа стойности от произволни типове при успех или неуспех.

Обикновено програмите или библиотеките на Gleam дефинират потребителски тип (custom type) с по един вариант (variant) за всяка възможна грешка заедно с информация за грешката, която би била полезна на програмиста.

Това е предимство пред изключенията (exceptions), защото веднага става ясно какви грешки може да върне дадена функция и компилаторът ще се увери, че се обработват. Край на неприятните изненади с неочаквани изключения!

Стойността на Result може да се обработва чрез съпоставяне с шаблони (pattern matching) в case израз, но с оглед на това колко често функциите връщат Result, това може да стане тромаво. Кодът на Gleam обикновено използва стандартната библиотека gleam/result и use изрази за работа с Result, и двете от които ще разгледаме в следващите глави.

import gleam/int
import gleam/io

pub fn main() {
  let _ = io.debug(buy_pastry(10))
  let _ = io.debug(buy_pastry(8))
  let _ = io.debug(buy_pastry(5))
  let _ = io.debug(buy_pastry(3))
}

pub type PurchaseError {
  NotEnoughMoney(required: Int)
  NotLuckyEnough
}

fn buy_pastry(money: Int) -> Result(Int, PurchaseError) {
  case money >= 5 {
    True ->
      case int.random(4) == 0 {
        True -> Error(NotLuckyEnough)
        False -> Ok(money - 5)
      }
    False -> Error(NotEnoughMoney(required: 5))
  }
}
</>Изпълняване на кода

Bit arrays

Битовите масиви (bit arrays) представляват поредица от 1-ци и 0-ли и са удобен синтаксис за конструиране и манипулиране на двоични данни.

Всеки сегмент на битов масив може да се зададат опции за това как е представен.

  • size (размер) - размерът на сегмента в битове.
  • unit (единица) - броят битове, от които стойността за size е кратна.
  • bits (битове) - влаложен битов масив с произволен размер.
  • bytes (байтове) - влаложен битов масив, подравнен към граници на байтове.
  • float (число с плаваща запетая) - 64-битово число с плаваща запетая.
  • int (цяло число) - цяло число, чийто размер по подразбиране е 8 бита.
  • big (голям) - голям ендиан (big endian).
  • little (малък) - малък ендиан (little endian).
  • native (платформен) - ендианът (endianness) на процесора.
  • utf8 - UTF8- енкодиран текст.
  • utf16 - UTF16- енкодиран текст.
  • utf32 - UTF32- енкодиран текст.
  • utf8_codepoint (UTF8 кодова точка) - UTF8 кодова точка.
  • utf16_codepoint (UTF16 кодова точка) - UTF16 кодова точка.
  • utf32_codepoint (UTF32 кодова точка) - UTF32 кодова точка.
  • signed (със знак) - число със знак.
  • unsigned (без знак) - число без знак.

Може да подадем множество опции на сегмент като ги разделим с тирета: x:unsigned-little-size(2).

Битовите масиви имат ограничена поддръжка, когато се компилират за JavaScript, не всички опции може да се използват. Пълна поддръжка ще се имплементира в бъдеще.

За повече информация за битовите масиви вижте документацията на Erlang за синтаксиса им.

import gleam/io

pub fn main() {
  // 8-битово цяло число. В двоичен вид: 00000011
  io.debug(<<3>>)
  io.debug(<<3>> == <<3:size(8)>>)

  // 16-битово цяло число. В двоичен вид: 0001100000000011
  io.debug(<<6147:size(16)>>)

  // Битов масив с UTF8 данни
  io.debug(<<"Hello, Joe!":utf8>>)

  // Конкатенация
  let first = <<4>>
  let second = <<2>>
  io.debug(<<first:bits, second:bits>>)
}
</>Изпълняване на кода

Standard library


Standard library package

Стандартната библиотека на Gleam е обикновен Gleam пакет, публикуван в хранилището Hex. Бихте могли да не я използвате, но почти всички Gleam проекти зависят от нея.

Всички модули, които сме импортирали досега в този урок, например gleam/io , са от стандартната библиотека.

Цялата документация за стандартната библиотека е достъпна в HexDocs. Сега ще разгледаме някои от най-често използваните модули.

import gleam/io

pub fn main() {
  io.println("Hello, Joe!")
  io.println("Hello, Mike!")
}
</>Изпълняване на кода

List module

Стандартната библиотека gleam/list съдържа функции за работа със списъци. Gleam програмите често ще я ползват и различните ѝ функции ще играят ролята на цикли за работа със списъци.

map създава нов списък като извиква подадена функция за всеки елемент на подадения списък.

filter (филтриране) създава нов списък, съдържащ само тези елементи на подадения списък, за които подадената функция връща True (вярно).

fold (сгъване?) комбинира всички елементи на списък в една стойност като извиква подадена функция отляво надясно за всеки елемент, подавайки резултата от предишното ѝ извикване като аргумент на следващото.

find (намиране) връща първия елемент на подадения списък, за който подадената функция връща True (вярно).

Струва си да се запознаете с всички функции в този модул, защото ще ги използвате често, когато пишете Gleam код!

import gleam/io
import gleam/list

pub fn main() {
  let ints = [0, 1, 2, 3, 4, 5]

  io.println("=== map ===")
  io.debug(list.map(ints, fn(x) { x * 2 }))

  io.println("=== filter ===")
  io.debug(list.filter(ints, fn(x) { x % 2 == 0 }))

  io.println("=== fold ===")
  io.debug(list.fold(ints, 0, fn(count, e) { count + e }))

  io.println("=== find ===")
  let _ = io.debug(list.find(ints, fn(x) { x > 3 }))
  io.debug(list.find(ints, fn(x) { x > 13 }))
}
</>Изпълняване на кода

Result module

Стандартната библиотека gleam/result съдържа функции за работа с Result. Gleam програмите ще я ползват често, за да се избегне прекомерното влагане на case изрази при извикване на функции, които може да се провалят (fail).

map актуализира стойността в Ok на Result, като ѝ прилага подадена функция. Ако Result-ът е Error (грешка), функцията не се извиква.

try извиква функция, която връща Result, за стойността в Ok на Result. Ако Result-ът е Error (грешка), функцията не се извиква. Това е удобно за верижно извикване на функции, които може да се провалят (fail), една след друга, спирайки на първата грешка.

unwrap взима стойността при успех от Result или връща стойност по подразбиране, ако е Error.

Функциите за работа с Result често се използват заедно с конвейери (pipelines), за да се верижно извикат няколко функции, връщащи Result.

import gleam/int
import gleam/io
import gleam/result

pub fn main() {
  io.println("=== map ===")
  let _ = io.debug(result.map(Ok(1), fn(x) { x * 2 }))
  let _ = io.debug(result.map(Error(1), fn(x) { x * 2 }))

  io.println("=== try ===")
  let _ = io.debug(result.try(Ok("1"), int.parse))
  let _ = io.debug(result.try(Ok("no"), int.parse))
  let _ = io.debug(result.try(Error(Nil), int.parse))

  io.println("=== unwrap ===")
  io.debug(result.unwrap(Ok("1234"), "default"))
  io.debug(result.unwrap(Error(Nil), "default"))

  io.println("=== pipeline ===")
  int.parse("-1234")
  |> result.map(int.absolute_value)
  |> result.try(int.remainder(_, 42))
  |> io.debug
}
</>Изпълняване на кода

Dict module

Стандартната библиотека gleam/dict (dict - речник) дефинира типа Dict (речник) и функции за работа с него. Речникът е колекция от ключове и стойности, наричана още хеш-таблица (hash map/table) в други езици за програмиране.

new (нов) и from_list (от списък) могат да се ползват за създаване на речници.

insert (вмъкване) и delete (изтриване) служат за добавяне и премахване на елементи от речник.

Както списъците, речниците са неизменни (immutable). При добавяне или премахване на елемент, се връща нов речник с добавения или премахнатия елемент.

Речниците не са наредени! Ако изглежда, че елементите са в някакъв ред, това е случайно и не бива да се разчита на него. Редът им може да се промени без предупреждение в бъдещи версии или в различни среди за изпълнение.

import gleam/dict
import gleam/io

pub fn main() {
  let scores = dict.from_list([#("Lucy", 13), #("Drew", 15)])
  io.debug(scores)

  let scores =
    scores
    |> dict.insert("Bushra", 16)
    |> dict.insert("Darius", 14)
    |> dict.delete("Drew")
  io.debug(scores)
}
</>Изпълняване на кода

Option module

В Gleam стойностите не може да са null, затова стандартната библиотека gleam/option дефинира типа Option , (опция) който може да се използва, за да представи стойност, която или я има, или я няма.

Типът Option е много подобен на Result, но няма стойност за грешка (error). В някои езици за програмиране функциите връщат Option, когато няма детайли за грешката, но Gleam винаги използва Result. Така всички функции, които могат да се провалят (fail) са консистентни, както и не се налага да се пише много излишен код за функции, които използват и двата типа.

import gleam/io
import gleam/option.{type Option, None, Some}

pub type Person {
  Person(name: String, pet: Option(String))
}

pub fn main() {
  let person_with_pet = Person("Al", Some("Nubi"))
  let person_without_pet = Person("Maria", None)

  io.debug(person_with_pet)
  io.debug(person_without_pet)
}
</>Изпълняване на кода

Advanced features


Opaque types

Непрозрачните типове (opaque types) са потребителски типове, самият тип е публичен (public) и може да се използва и от други модули, но конструкторите му са частни (private) и могат да се използват само в модула, в който е дефиниран типът. По този начин другите модули не може да конструират стойности от този тип, нито да правят съпоставяне с шаблони върху него.

Това е полезно, когато създаваме типове с интелигентни конструктори (smart constructors). Интелигентният конструктор е функция, която създава стойност от даден тип, но с повече ограничения от тези, които би имало, ако програмистът можеше директно да извика конструкторите на типа. Така е по- лесно да се гарантира, че типът се използва правилно.

Например, този потребителски тип PositiveInt (положително цяло число) е непрозрачен (opaque). Другите модули е нужно да използват функцията new (нов) за да конструират стойност от този тип. Тази функция гарантира, че цялото число е положително.

import gleam/io

pub fn main() {
  let positive = new(1)
  let zero = new(0)
  let negative = new(-1)

  io.debug(to_int(positive))
  io.debug(to_int(zero))
  io.debug(to_int(negative))
}

pub opaque type PositiveInt {
  PositiveInt(inner: Int)
}

pub fn new(i: Int) -> PositiveInt {
  case i >= 0 {
    True -> PositiveInt(i)
    False -> PositiveInt(0)
  }
}

pub fn to_int(i: PositiveInt) -> Int {
  i.inner
}
</>Изпълняване на кода

Use

Gleam няма изключения (exceptions), макроси (macros), type classes (класове типове?), ранно връщане от функции (early returns) и разнообразни други инструменти, залагайки изцяло на функции, които са first-class citizens и съпоставяне с шаблони (pattern matching). Това прави кода на Gleam по-лесен за разбиране, но понякога води до прекомерно влагане (indentation).

use изразите в Gleam помагат да пишем код с функции за обратна връзка (callbacks) без прекомерно влагане, както е показано в кода.

Функцията от по-висок ред (higher order function), която се извиква стои отдясно на оператора <-. Тази функция е задължително да приема функция за обратна връзка (callback) като последен аргумент.

Имената на аргументите на функцията за обратна връзка (callback) се пишат отляво на оператора <-. Функцията може да приема произволен брой аргументи, включително и нула.

Целият останал код в обхващащия го блок {} става тяло на функцията за обратна връзка (callback).

use е много мощен и полезен инструмент, но прекомерното му използване може да направи кода неразбираем, особено за начинаещи. Обикновено кодът е по-ясен, ако използваме нормалния синтаксис за извикване на функции!

import gleam/io
import gleam/result

pub fn main() {
  let _ = io.debug(without_use())
  let _ = io.debug(with_use())
}

pub fn without_use() {
  result.try(get_username(), fn(username) {
    result.try(get_password(), fn(password) {
      result.map(log_in(username, password), fn(greeting) {
        greeting <> ", " <> username
      })
    })
  })
}

pub fn with_use() {
  use username <- result.try(get_username())
  use password <- result.try(get_password())
  use greeting <- result.map(log_in(username, password))
  greeting <> ", " <> username
}

// Ето няколко примерни функции: 

fn get_username() {
  Ok("alice")
}

fn get_password() {
  Ok("hunter2")
}

fn log_in(_username: String, _password: String) {
  Ok("Welcome")
}
</>Изпълняване на кода

Use sugar

use изразите са синтактична захар (syntactic sugar) за обикновено извикване на функция и анонимна функция (anonymous function).

Този код:

  use a, b <- my_function
  next(a)
  next(b) 
  

се преобразува до този код:

  my_function(fn(a, b) {
  next(a)
  next(b)
  })
  

За да сме сигурни, че кодът ни с use работи и е възможно най-разбираем, в идеалния случай в дясната част на израза трябва да има само извикване на функция, a не конвейер (pipeline) или друг израз, защото е по-трудно за четене.

use е израз (expression), както всичко друго в Gleam, затова може да го слагаме в блокове.

import gleam/io
import gleam/result

pub fn main() {
  let x = {
    use username <- result.try(get_username())
    use password <- result.try(get_password())
    use greeting <- result.map(log_in(username, password))
    greeting <> ", " <> username
  }

  case x {
    Ok(greeting) -> io.println(greeting)
    Error(error) -> io.println("ERROR:" <> error)
  }
}

// Ето няколко примерни функции: 

fn get_username() {
  Ok("alice")
}

fn get_password() {
  Ok("hunter2")
}

fn log_in(_username: String, _password: String) {
  Ok("Welcome")
}
</>Изпълняване на кода

Todo

Ключовата дума todo (за правене) се използва, за да отбележим части от кода, които още не сме имплементирали.

as "some string" (като "някакъв символен низ") е по желание, но е добре да добавяме съобщение (message), ако имаме повече от един блок, отбелязан като todo.

При използване на todo компилаторът на Gleam ще покаже предупреждение (warning), за да ни подсети, че кодът е незавършен и, ако се опитаме да го стартираме, програмата ще се срине с подаденото съобщение (message).

pub fn main() {
  todo as "I haven't written this code yet!"
}

pub fn todo_without_reason() {
  todo
}
</>Изпълняване на кода

Panic

Ключовата дума panic (паника) е подобна на todo, но се използва, за да се срине програмата, когато се стигне до състояние, до което не би трябвало никога да се стига.

Тази ключова дума почти никога не трябва да се използва! Може да е полезна в първоначални прототипи и скриптове, но използването ѝ в библиотека или в приложение, което ще се ползва в реална среда, е признак за лош дизайн (design). Ако типовете (types) са добре проектирани, системата за проверка на типовете (types) може да се използва, за да се направят такива невалидни състояния непредставими (unrepresentable).

import gleam/io

pub fn main() {
  print_score(10)
  print_score(100_000)
  print_score(-1)
}

pub fn print_score(score: Int) {
  case score {
    score if score > 1000 -> io.println("High score!")
    score if score > 0 -> io.println("Still working on it")
    _ -> panic as "Scores should never be negative!"
  }
}
</>Изпълняване на кода

Let assert

let assert (нека твърдим/проверяваме/гарантираме) е последният начин умишлено да сринем Gleam програмата си. Той е подобен на ключовата дума panic, с това че срива програмата когато се стигне до състояние, до което не би трябвало да се стига.

let assert е подобен на let по това, че служи за присвояване на стойности на променливи, но е различен с това, че шаблонът може да е частичен (partial). Шаблонът не е нужно да съвпада с всички възможни стойности от типа, от който се присвоява.

Както и при panic, let assert трябва да се използва рядко, вероятно изобщо не бива да се ползва в библиотеки.

import gleam/io

pub fn main() {
  let a = unsafely_get_first_element([123])
  io.debug(a)

  let b = unsafely_get_first_element([])
  io.debug(b)
}

pub fn unsafely_get_first_element(items: List(a)) -> a {
  // Това ще предизвика panic, ако списъкът е празен.
  // Обикновен `let` не би позволил този частичен шаблон 
  let assert [first, ..] = items
  first
}
</>Изпълняване на кода

Externals

Понякога в проектите си искаме да ползваме код, написан на други езици за програмиране, най-често Erlang и JavaScript, в зависимост от средата за изпълнение (runtime), която ползваме. Gleam позволява да импортираме и да използваме външен код чрез external functions (външни функции) и external types (външни типове).

External type е тип, който няма конструктори. Gleam не знае как е устроен нито как да се създаде стойност от него. Зне само че съществува такъв тип.

External function (външна функция) е функция, на която е зададен атрибутът @external (външен). Този атрибут инструктира компилатора да използва модулната функция (module function), указана в атрибута, вместо да търси Gleam код за тази функция.

Компилаторът не може да провери какви са типовете на аргументите и връщаната стойност на функциите в други езици, затова когато използваме атрибута @external (външен), е нужно да указваме типовете (types) в Gleam код. Gleam "вярва", че посочените типове са правилни и грешки в тях биха довели до неочаквано поведение или сривове по време на изпълнение (runtime). Бъдете внимателни!

Външните функции (external functions) са полезни, но е добре да ги използваме пестеливо. Навсякъде, където е възможно пишете Gleam код!

import gleam/io

// Тип без Gleam конструктори
pub type DateTime

// Външна функция, която създава стойност от типа
@external(javascript, "./my_package_ffi.mjs", "now")
pub fn now() -> DateTime

// Функцията `now` в `./my_package_ffi.mjs` е такава:
// export function now() {
//   return new Date();
// }

pub fn main() {
  io.debug(now())
}
</>Изпълняване на кода

Multi target externals

Може да указваме по няколко external (външни) имплементации за една и съща функция, което ни позволява да я ползваме и с Erlang, и с JavaScript.

Ако една функция няма external (външна) имплементация за целта (target), за която в момента се компилира, компилаторът ще покаже грешка.

Винаги се стремете да имплементирате функциите си за всички цели (targets), но не винаги това е възможно заради различия в начина, по който се извършва вход/изход (input/output) и паралелност (concurrency) в Erlang и JavaScript. В Erlang паралелният вход/изход се управлява от средата за изпълнение (runtime) автоматично (transparently), а в JavaScript паралелният вход/изход изисква използването на обещания (promises) или функции за обратна връзка (callbacks). Ако вашият код е написан в стил Erlang, обикновено е невъзможно да се имплементира и в JavaScript. Ако се използват функции за обратна връзка, няма да е съвместим с повечето Gleam и Erlang код, защото така всеки код, който извиква тази функция също ще е нужно да ползва функции за обратна връзка.

Библиотеките, които ползват паралелен вход/изход (I/O) обикновено е нужно да изберат дали ще поддържат Erlang или JavaScript, документирайки този избор в README файла на библиотеката.

import gleam/io

pub type DateTime

@external(erlang, "calendar", "local_time")
@external(javascript, "./my_package_ffi.mjs", "now")
pub fn now() -> DateTime

pub fn main() {
  io.debug(now())
}
</>Изпълняване на кода

External gleam fallbacks

Една функция може да има и Gleam имплементация и external (външна) имплементация. Ако има external имплементация за целта (target), за която се компилира в момента, тя ще бъде използвана. Иначе ще се ползва Gleam имплементацията.

Това е полезно, ако една функция може да се имплементира в Gleam, но за една от целите (targets) имаме оптимизирана имплементация. Например, виртуалната машина на Erlang (Erlang VM) има вградена функция, която обръща списък (reverse). Тази функция е имплементирана в нативен код. Кодът тук я използва, когато се изпълнява в Erlang, тъй като тогава е налична.

import gleam/io

@external(erlang, "lists", "reverse")
pub fn reverse_list(items: List(e)) -> List(e) {
  tail_recursive_reverse(items, [])
}

fn tail_recursive_reverse(items: List(e), reversed: List(e)) -> List(e) {
  case items {
    [] -> reversed
    [first, ..rest] -> tail_recursive_reverse(rest, [first, ..reversed])
  }
}

pub fn main() {
  io.debug(reverse_list([1, 2, 3, 4, 5]))
  io.debug(reverse_list(["a", "b", "c", "d", "e"]))
}
</>Изпълняване на кода