Базовые типы

В этой главе мы узнаем больше о базовых типах Эликсира: целых числах, числах с плавающей точкой, логических или булевых значениях, атомах, строках, списках и кортежах. Вот некоторые базовые типы:

iex> 1          # Целое число
iex> 0x1F       # Целое число
iex> 1.0        # Число с плавающей точкой
iex> true       # Логическое значение
iex> :atom      # Атом
iex> "elixir"   # Строка
iex> [1, 2, 3]  # Список
iex> {1, 2, 3}  # Кортеж

Базовая арифметика

Откройте IEx и введите следующие выражения:

iex> 1 + 2
3

iex> 5 * 5
25

iex> 10 / 2
5.0

Обратите внимание, что выражение 10 / 2 вернёт число с плавающей точкой 5.0, а не целое 5. Это ожидаемо. В Эликсире оператор / всегда возвращает float. Если вы хотите совершить целочисленное деление или получить остаток от деления, вы можете использовать функции div и rem (от англ. division и remainder):

iex> div(10, 2)
5

iex> div 10, 2
5

iex> rem 10, 3
1

Эликсир позволяет опустить скобки при вызове именованных функций. Такая возможность позволяет получить более читаемый синтаксис при написании декларативных спецификаций и конструкций управления потоком.

Эликсир также поддерживает краткую форму написания двоичных, восьмиричных и шестнадцатеричных чисел:

iex> 0b1010
10

iex> 0o777
511

iex> 0x1F
31

Числа с плавающей запятой обязательно должны содержать точку и хотя бы одну цифру после неё, а также поддерживают e-нотацию для экспоненциальных чисел:

iex> 1.0
1.0

iex> 1.0e-10
1.0e-10

Числа с плавающей точкой в Эликсире занимают 64 бита и имеют двойную точность.

Вы можете выполнить функцию round, чтобы получить ближайшее целое число к аргументу с типом float, или функцию trunc, которая вернёт целую часть от числа.

iex> round(3.58)
4

iex> trunc(3.58)
3

Идентификация функций

Функции в Эликсире идентифицируются по их именам и их арности. Арность функции – это количество аргументов, которые принимает функция. С этого момента мы будем использовать как имя функции, так и её арность для обозначения функции во всей документации. Обозначение round/1 обозначает функцию с именем round, которая принимает один аргумент, в то время как round/2 обозначает другую (несуществующую) функцию с тем же именем, но с арностью два.

Логические значения

Эликсир поддерживает true и false в качестве логических значений:

iex> true
true

iex> true == false
false

Эликсир предоставляет набор функций для проверки типа. Например, функция is_boolean/1 может быть использована для проверки, является значение логическим или нет:

iex> is_boolean(true)
true

iex> is_boolean(1)
false

Вы можете также использовать функции is_integer/1, is_float/1 или is_number/1, чтобы проверить, является ли аргумент целым числом, дробным числом или одним из них.

Помните. В любой момент вы можете набрать h() в интерактивной оболочке и получить информацию, как её использовать. Хелпер h может также быть использован для доступа к документации по любой функции. Например, команда h is_integer/1 напечатает документацию функции is_integer/1. Это также работает для операторов и других конструкций (попробуйте h ==/2).

Атомы

Атомы – это константы, имя которых является также и их значением. Некоторые другие языки называют их символами:

iex> :hello
:hello

iex> :hello == :world
false

Логические значения true и false, фактически, являются атомами:

iex> true == :true
true

iex> is_atom(false)
true

iex> is_boolean(:false)
true

Строки

Строки в Эликсире располагаются внутри двойных кавычек, и они представлены в кодировке UTF-8:

iex> "hellö"
"hellö"

Обратите внимание. Если вы работаете под Виндоус, ваш терминал может использовать отличную от UTF-8 кодировку по умолчанию. Вы можете сменить кодировку текущей сессии командой chcp 65001 перед запуском IEx.

Эликсир также поддерживает интерполяцию строк:

iex> "hellö #{:world}"
"hellö world"

Строки могут содержать разрывы. Вы можете ввести их, используя экранированные последовательности:

iex> "hello
...> world"
"hello\nworld"

iex> "hello\nworld"
"hello\nworld"

Вы можете вывести строку, используя функцию IO.puts/1 из модуля IO:

iex> IO.puts "hello\nworld"
hello
world
:ok

Помните, что функция IO.puts/1 возвращает атом :ok в качестве результата после вывода.

Строки в Эликсире представлены внутри двоичными данными, которые являются последовательностями байтов:

iex> is_binary("hellö")
true

Мы также можем получить количество байт в строке:

iex> byte_size("hellö")
6

Обратите внимание, что количество байт в данной строке равно 6, хотя в ней 5 символов. Дело в том, что символ «ö» занимает 2 байта, чтобы быть представленным в UTF-8. Мы можем получить реальную длину строки, основанную на количестве символов, использовав функцию String.length/1:

iex> String.length("hellö")
5

Модуль String содержит набор функций для манипуляций со строками, удовлетворяющий стандарту Юникод:

iex> String.upcase("hellö")
"HELLÖ"

Анонимные функции

Анонимные функции могут быть созданы в одну строку. Они заключаются между ключевыми словами fn и end:

iex> add = fn a, b -> a + b end
#Function<12.71889879/2 in :erl_eval.expr/5>

iex> add.(1, 2)
3

iex> is_function(add)
true

iex> is_function(add, 2) # проверяет, что `add` – функция, принимающая ровно два аргумента
true

iex> is_function(add, 1) # проверяет, что `add` – функция, принимающая ровно один аргумент
false

Функции в Эликсире являются сущностями первого класса, то есть они могут быть переданы в качестве аргумента другой функции, так же, как целые числа и строки. В примере мы передали функции is_function/1 переменную add и она вернула true. Мы можем также проверить арность функции вызовом is_function/2.

Обратите внимание, что точка . между переменной и скобками обязательна для вызова анонимной функции. Эта точка нужна, чтобы избавиться от неоднозначности между анонимной функцией add и именованной add/2. В этом смысле в Эликсире явно различаются анонимные и именованные функции. Мы поговорим об этой разнице в главе «Модули и функции».

Анонимные функции являются замыканиями и по существу они могут иметь доступ к переменным из области видимости, когда функция объявлена. Давайте объявим новую анонимную функцию, использующую анонимную функцию add, которую мы объявили ранее:

iex> double = fn a -> add.(a, a) end
#Function<6.71889879/1 in :erl_eval.expr/5>

iex> double.(2)
4

Помните, что присвоение значений внутри функции никак не изменяют её окружение:

iex> x = 42
42

iex> (fn -> x = 0 end).()
0

iex> x
42

(Связные) Списки

Эликсир использует квадратные скобки, чтобы задать список значений. Значения могут быть любого типа:

iex> [1, 2, true, 3]
[1, 2, true, 3]

iex> length [1, 2, 3]
3

Два списка могут быть сложены, используя оператор ++/2, и один может быть вычтен из другого с помощью --/2:

iex> [1, 2, 3] ++ [4, 5, 6]
[1, 2, 3, 4, 5, 6]

iex> [1, true, 2, false, 3, true] -- [true, false]
[1, 2, 3, true]

В этом руководстве мы будем много говорить о голове и хвосте списка. Головой называют первый элемент списка, а хвостом – его оставшуюся часть. Они могут быть получены функциями hd/1 и tl/1. Давайте создадим список и получим его голову и хвост:

iex> list = [1, 2, 3]

iex> hd(list)
1

iex> tl(list)
[2, 3]

Попытка получить голову или хвост пустого списка выбросит ошибку:

iex> hd []
** (ArgumentError) argument error

Иногда вы будете создавать список и он будет возвращать значение в одинарных кавычках. Например:

iex> [11, 12, 13]
'\v\f\r'

iex> [104, 101, 108, 108, 111]
'hello'

Когда Эликсир видит список корректных ASCII-кодов, которые может напечатать, он выводит список символов (в буквальном смысле). Списки символов часто используются для взаимодействия с существующим кодом на Эрланге. Если вы видите значение в IEx, и вы не уверены, что это такое, вы можете использовать i/1 для получения информации:

iex> i 'hello'
Term
  'hello'
Data type
  List
Description
  ...
Raw representation
  [104, 101, 108, 108, 111]
Reference modules
  List

Помните, что значения в одинарных и двойных кавычках не эквивалентны в Эликсире, они принадлежат разным типам:

iex> 'hello' == "hello"
false

Внутри одиночных кавычек списки символов, внутри двойных – строки. Мы поговорим об этом больше в главе «Двоичные данные, строки и списки символов».

Кортежи

Эликсир использует фигурные скобки для объявления кортежей. Как и списки, кортежи могут хранить любые значения:

iex> {:ok, "hello"}
{:ok, "hello"}

iex> tuple_size {:ok, "hello"}
2

Кортежи хранят элементы в памяти последовательно. Это значит, что доступ к элементу кортежа по индексу или получение размера кортежа – быстрая операция. Индексы начинаются с нуля:

iex> tuple = {:ok, "hello"}
{:ok, "hello"}

iex> elem(tuple, 1)
"hello"

iex> tuple_size(tuple)
2

Также можно добавить элемент на определённое место в кортеже с помощью функции put_elem/3:

iex> tuple = {:ok, "hello"}
{:ok, "hello"}

iex> put_elem(tuple, 1, "world")
{:ok, "world"}

iex> tuple
{:ok, "hello"}

Обратите внимание, что функция put_elem/3 вернёт новый кортеж. Исходный кортеж, который хранится в переменной tuple, не изменяется, потому что данные в Эликсире иммутабельны. Благодаря этому, вам не придётся беспокоиться, что какой-то код может изменить ваши структуры данных.

Списки или кортежи?

Какая разница между списками и кортежами?

Списки хранятся в памяти, как связные списки, это значит, что каждый элемент списка хранит своё значение и указывает на следующий элемент, пока не будет достигнут конец списка.

iex> list = [1 | [2 | [3 | []]]]
[1, 2, 3]

Это значит, что получение длины списка – линейная операция: нам нужно пройти через весь список последовательно, чтобы узнать его размер. Обновление списка – быстрая операция, также как добавление элементов в начало:

iex> [0 | list]
[0, 1, 2, 3]

Кортежи, с другой стороны, хранятся в памяти последовательно. Это значит, что получение размера кортежа или доступ к элементу по индексу работают быстро. Однако, обновление или добавление элементов в кортеже – дорогая операция, поскольку включает в себя копирование всего кортежа в память.

Использование той или иной структуры данных продиктовано особенностями их производительности. Наиболее частый случай использования кортежей – возврат дополнительной информации из функции. Например, File.read/1 – функция, которая используется для чтения содержимого файлов. Она возвращает кортежи:

iex> File.read("path/to/existing/file")
{:ok, "... contents ..."}

iex> File.read("path/to/unknown/file")
{:error, :enoent}

Если путь, переданный в функцию File.read/1, существует, она возвращает кортеж с атомом :ok в качестве первого элемента и содержимым файла в качестве второго. Иначе, она возвращает кортеж с :error и описанием ошибки.

Большую часть времени Эликсир пытается направить вас поступать правильно. Например, есть функция elem/2 для доступа к элементу кортежа, но нет встроенного эквивалента этой функции для списков:

iex> tuple = {:ok, "hello"}
{:ok, "hello"}

iex> elem(tuple, 1)
"hello"

При подсчёте элементов в структурах данных, Эликсир также придерживается простого правила: функция называется size, если операция выполняется за константное время (значение предварительно посчитано), или length, если операция линейна (подсчёт длины тем дольше, чем больше переданное значение). Удобно запомнить, что «length» и «linear» оба начинаются с «l».

Например, мы уже использовали 4 функции подсчёта: byte_size/1 (количество байт, занимаемых строкой), tuple_size/1 (размер кортежа), length/1 (длина списка), String.length/1 (количество графем в строке). Мы использовали byte_size для получения количества байт, которые занимает строка – это дешёвая операция. Для получения количества символов Юникода, с другой стороны, используется String.length, что может быть дорого, т. к. для этого нужен обход всей строки.

Эликсир также предоставляет модули Port, Reference, и PID как типы данных (обычно используются при общении процессов), и мы уделим им немного внимания, когда будем говорить о процессах. Теперь давайте познакомимся с базовыми операторам, которые используются с нашими базовыми типами.

© 2020 / Россия Любые мысли и вопросы пишите на elixir@wunsh.ru.