Предыдущие главы

«Глава 1 | Зачем беспокоиться?"

Сфера действия этой главы

Как я упоминал в предыдущей главе, изучение Phoenix - это здорово, потому что мы можем изучать Elixir (язык, на котором он построен) в процессе. Однако это также означает, что мы должны приложить немного больше усилий, прежде чем погрузиться в Phoenix, чтобы понять все основы этого нового языка программирования.

Elixir - это функциональный язык программирования по своей сути, который имеет несколько замечательных функций, позволяющих упаковать большие возможности в более короткие объемы кода.

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

Я не только надеюсь, что эта глава представляет собой практическое руководство по изучению Elixir, но также надеюсь, что вы видите, что Elixir - это очень весело!

Давайте начнем.

Прежде, чем мы начнем

Обязательно установите Elixir, следуя официальным инструкциям.

Вы можете проверить правильность установки Elixir, выполнив в командной строке следующее:

elixir -v

Если он установлен правильно, мы можем запустить код Elixir в интерактивной оболочке, которую вы должны запустить, запустив:

iex

Еще одно замечание: я собирался ответить на вопрос Что такое функциональное программирование? поскольку Эликсир - это функциональный язык. Я просто собираюсь перейти к этой замечательной статье Эрика Эллиота, если вы не знакомы.

Типы данных

Вот сравнение основных типов данных Elixir и типов данных JavaScript:

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

Целые числа

Целые числа - это целые числа. В Elixir они могут быть отрицательными или положительными:

> 2 + 2
4
> -2 + 2
0

Плавающие

Floats - это числа с хотя бы одним десятичным знаком:

> 2.0 + 3.4
5.4

Их также можно выразить в экспоненциальной форме:

> 1.0e-9
1.0e-9

Подумайте о научных обозначениях из вашего школьного класса математики:

Основы арифметики

Теперь, когда у нас есть два числовых типа, давайте попробуем пример базовой арифметики:

> 2 + 2
4
> 2.0 + 4
6.0
> 2 - 4
-2
> 2.0 - 4
-2.0
> 2 * 3
6
> 2 * 4.0
8.0
> 2 / 2
2.0
> 2/1.1
1.818181...

В первом примере мы выполняем простое сложение между двумя целыми числами, и в результате получается целое число. Однако следующий пример показывает нам, что число с плавающей запятой плюс целое число дает число с плавающей запятой.

То же самое мы видим в следующих нескольких примерах вычитания и умножения.

Деление немного другое. 2 / 2 возвращает значение с плавающей запятой, 2.o. Для целочисленного деления необходимо использовать div:

> div(2,2)
1
> div 2,2
1

Мы также можем получить остаток, используя rem:

> rem 26,5
1

Если пример rem кажется странным, вот визуализация:

Наконец, давайте округлим число с плавающей запятой до целого, например:

> round(2.2)
2

Булевы

Как и в случае с JavaScript, логические значения могут быть истинными или ложными:

> true
true
> false
false

Единственное, что неверно (кроме ложного), это nil:

> nil
nil

Атомы и модули

Атомы - это константы, имена которых также являются их значениями:

> :test
:test
> :test == :compare
false

В JavaScript эквиваленты symbols, которые были введены в ES6:

var sym = Symbol('foo');

Их единственная цель в JavaScript - быть идентификатором свойств объекта.

В Elixir имена модулей также являются атомами.

Модули - это пространства имен для функций. Мы рассмотрим их позже в этой главе. Однако, если бы у нас был модуль с именем Test и функция внутри этого модуля с именем func, мы бы назвали его вот так (не запускайте это):

> Test.func
*result from function*

Даже Test еще не объявлен, он тоже будет атомом:

> is_atom(Test)
true

Атомы также можно использовать для ссылки на внешние и встроенные библиотеки Erlang:

Струны

Строки довольно легко понять. Однако строки должны быть заключены в двойные кавычки:

> "string"
"string"

Мы можем печатать строки, используя IO.puts (обозначает ввод / вывод):

> IO.puts "hello"
hello
:ok

Как видите, строка также выводит атом :ok.

Мы также можем использовать IO.gets для захвата строки из ввода:

> myFav = IO.gets "Elixir or JavaScript?\n"
Elixir or JavaScript? //prompt
Elixir //what we type in
"Elixir\n" //captured string
> myFav
"Elixir\n"

В приведенном выше коде мы отображаем приглашение, вводим наш ответ, видим захваченную строку, а затем проверяем значение myFav, равное Elixir\n.

\n вставляет новую строку при печати. Вы можете думать об этом как о <br/> в HTML.

Мы можем увидеть это в действии, напечатав myFav:

> IO.puts myFav
Elixir
:ok

Эквивалент + (JavaScript) для конкатенации строк в Elixir - <>:

> IO.puts "Hello" <> " World"
Hello World
:ok

Операторы сравнения

Давайте посмотрим на операторы сравнения. Для разработчика JavaScript нет ничего необычного:

> 2 == 2.0
true
> 2 === 2.0
false
> 2 > 3
false
> 2 < 4
true
> 2 <= 2
true
> 4 >= 3
true
> 2 != 2
false
> 2 !== 2.0
true

=== и !== - это строгие сравнения, которые проверяют тип и значение. Как мы видели в примерах выше,

2 === 2.0 неверно, потому что в Elixir есть целые числа и числа с плавающей запятой, в отличие от JavaScript, в котором просто число.

Мы также можем выполнять сравнение строк любых типов:

> 2 === "string"
false

Списки

Списки - это наборы значений, которые могут содержать в скобках несколько типов:

> list = [1, "a", :test]
[1, "a", :test]
> list
[1, "a", :test]

В Elixir списки реализованы как связанные списки. Связанные списки - это линейные структуры данных, в которых каждый элемент является отдельным узлом. [1]

Визуально они бы выглядели так:

Чтобы прочитать все значения в связанном списке, мы перемещаемся от начала до конца (где next равно нулю). Заголовок - это первое значение, а остальная часть списка - это хвост.

Мы можем делать некоторые интересные вещи со списками, например, конкатенацию, используя ++:

> list = [1,2] ++ [3,4]
[1, 2, 3, 4]

Примечание. В большинстве случаев вам следует присоединить значение к началу списка (добавить) следующим образом:

> list = [1,2,3]
[1, 2, 3]
> ["soup"] ++ list
["soup", 1, 2, 3]

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

> list -- [1,2]
[3, 4]

Еще одна интересная особенность заключается в том, что мы можем легко извлечь начало и конец списка, используя hd и tl:

> hd [3,2,1]
3
> tl [3,2,1]
[2,1]

Мы также можем очень легко сохранить значения заголовка и хвоста из списков, используя сопоставление с образцом:

> [storeA | storeB] = [1, "chicken", "soup"]
[1, "chicken", "soup"]
> storeA
1
> storeB
["chicken", "soup"]

Кортежи

Согласно Elixir School, кортежи похожи на списки, но хранятся в памяти непрерывно. Я уверен, что это может не сразу иметь смысл для всех, так что вот полное объяснение.

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

{3.14, :pie, "Apple"}

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

Мы можем извлечь значение из кортежа, используя elem(*tuple*, *index*):

> tuple = {1,2}
{1, 2}
> elem(tuple, 0)
1

Обратите внимание, что индекс начинается с 0.

Списки ключевых слов

Списки ключевых слов - это списки, которые связывают ключ со значением:

> [chicken: "noodle", soup: "is lit"]
[chicken: "noodle", soup: "is lit"]

В приведенном выше примере chicken: и soup: являются атомами. chicken: “noodle” и soup: “is lit” являются кортежами.

Пример также можно было бы записать так:

> [{:chicken, "noodle"}, {:soup, "is lit"}]
[chicken: "noodle", soup: "is lit"]

Карты

Карты - еще одна ассоциативная коллекция (хранилище с ассоциацией ключей и значений). Карты определяются с помощью %{}:

> example = %{:hey => "you", "squadron" => :up}
%{:hey => "you", "squadron" => :up}
> example[:hey]
"you"
> example["squadron"]
:up

Мы также можем получить значение, которое хранится в кортеже с предшествующим атомом :ok:

> Map.fetch(example, :hey)
{:ok, "you"}

Перечисление

Elixir поставляется со встроенным модулем Enum, который позволяет нам перечислять списки, списки ключевых слов и карты, чтобы делать некоторые полезные вещи.

Давай проверим это.

Enum.all?

Мы можем использовать Enum.all? для перечисления в коллекции и возврата true, если каждый элемент в коллекции соответствует условию.

Например:

> list = ["foo", "bar", "hello"]
["foo", "bar", "hello"]
> Enum.all?(list, fn(item) -> String.length(item) == 3 end)
false

Пара замечаний.

Во-первых, приведенный выше код можно прочитать как: «Пронумеровать каждый элемент в нашем списке и вернуть истину, если каждый элемент является строкой из 3 символов.

Во-вторых, обратите внимание на синтаксис этого:

Enum.all?(*insert list, *anonymous function*)

Анонимная функция в этом примере fn(item) -> String.length(item) == 3 end похожа на следующую в JavaScript:

list.map((item) => {....})

Есть пара отличий:

  • fn стоит перед параметром.
  • Стрелка «тощая», а не «толстая».
  • Заявление о возврате нам не требуется.
  • Мы завершаем анонимную функцию с помощью end.

Enum.any?

Это похоже на предыдущий пример, за исключением того, что он вернет истину, если какой-либо элемент соответствует условию:

> Enum.any?(list, fn(item) -> String.length(item) == 3 end)
true

Enum.chunk

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

Enum.chunk(*insert collection*, *break into groups of this size*)

Например:

> Enum.chunk([1, 2, 3, 4], 2)
[[1,2], [3,4]]

Примечание. Если осталось недостаточно элементов для формирования группы указанного размера, оставшиеся элементы не включаются:

> Enum.chunk(list, 2)
[["foo", "bar"]]
> list
["foo", "bar", "hello"]

Мы также можем выполнить фрагмент на основе условия, отличного от размера, с помощью Enum.chunk_by:

> Enum.chunk_by(list, fn(item) -> String.length(item) == 3 end)
[["foo", "bar"], ["hello"]]

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

> Enum.chunk_by(["one", "two", "three", "four", "five"], fn(x) -> String.length(x) end)
> [["one", "two"], ["three"], ["four", "five"]]

Enum.map_every

Что, если бы мы хотели просмотреть коллекцию и что-то сделать с каждым n-м элементом? Вот тут-то и пригодится Enum.map_every :

> numbers = [1,2,3]
[1, 2, 3]
> Enum.map_every(numbers, 2,  fn(number) -> number + 1 end)
[2, 2, 4]

Как видите, он применяет функцию к первому элементу и каждому n-му элементу после него.

Enum.each

Это используется, если мы хотим перебрать коллекцию, но не создать новую коллекцию.

Например, мы можем распечатать список:

> Enum.each(list, fn(item) -> IO.puts(item) end)
Foo
Bar
Hello
:ok

Enum.map

Если мы действительно хотим перебрать коллекцию и создать новую коллекцию с обновленными значениями, мы можем использовать Enum.map:

> numbers = [1, 2, 3, 4]
[1, 2, 3, 4]
> Enum.map(numbers, fn(number) -> number * 2 end)
[2, 4, 6, 8]

Enum.min и Enum.max

Как вы можете догадаться, они возвращают минимум и максимум коллекции соответственно:

> Enum.min(numbers)
1
> Enum.max(numbers)
4

Enum.reduce

Enum.reduce принимает коллекцию и аккумулятор и «сокращает» коллекцию до одного числа. Есть смысл? Нет, Майк!

Я полагал. Давайте объясним на примере:

> Enum.reduce([1, 2, 3], 5, fn(item, acc) -> item + acc end)
> 11

Что за стрельба ?! Как нам дали 11 ?!

Давайте пройдемся по нему на каждой итерации:

1) item is 1 and acc is 5. 6 (1 + 5) then becomes the new value of the accumulator.
accumulator: 6
2) item is now 2 and acc is 6. 8 (2 + 6) then becomes the new value of the accumulator.
accumulator: 8
3) item is now 3 and acc is 8. 11 (3 + 8) then becomes the new value of the accumulator.
Final accumulator and final value returned: 11

В приведенном выше примере накапливается одно значение и возвращается, следовательно, аккумулятор.

Если аккумулятор не указан, он инициализируется как 0

> Enum.reduce([1, 2, 3], fn(item, acc) -> item + acc end)
6

Enum.sort

Мы можем использовать это для сортировки коллекции:

> numbers = [4,99,1]
[4, 99, 1]
> sorted = Enum.sort(numbers)
[1, 4, 99]
> sorted
[1, 4, 99]

Enum.uniq

Последнее, что нужно обсудить, - это Enum.uniq, который просто удаляет повторяющиеся значения:

> dups = [4,1,2,1]
[4, 1, 2, 1]
> Enum.uniq(dups)
[4, 1, 2]

Как видите, дубликаты удаляются с конца.

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

Не волнуйся. На этом веселье не закончится.

Соответствие шаблону

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

numbers = [1, 2, 3, 4]

Что ж, = - это не то, к чему вы, возможно, привыкли в JavaScript и других языках. Помимо привязки переменных, его можно использовать для хранения значений из структур данных в отдельных переменных.

Вот пример:

> [a,b,c] = [1,2,3]
[1, 2, 3]
> a 
1
> b
2
> c 
3

Аккуратный!

Теперь это работает, только если обе стороны одного размера и одного типа:

> [a,b] = [1,2,3]
** (MatchError) no match of right hand side value: [1, 2, 3]
> {a,b,c} = [1,2,3]
** (MatchError) no match of right hand side value: [1, 2, 3]

Вторая ошибка возникает из-за того, что левая коллекция является кортежем, а правая - списком.

Структуры управления

Структуры управления могут показаться сложными, но мы просто рассмотрим условия обработки.

Если еще

if / else в Эликсире имеет следующий синтаксис:

if *condition is true* do
  //something
else
  //do something else
end

Давайте попробуем:

> x = 1
1
> y = 2
2
> if x === y do
...> IO.puts "Cool beans!"
...> else
...> IO.puts "Oh shoot!"
...> end
Oh shoot!
:ok

Мы также могли бы сделать if/else if/else, который выглядел бы так:

Пока не

Вот классная структура управления, отличная от JavaScript.

Мы можем заменить if на если не так:

> game = "mass effect andromeda"
"mass effect andromeda"
> unless game = "mass effect andromeda" do
...> IO.puts "Sure! I'll play!"
...> else 
...> IO.puts "No thanks!"
...> end
No thanks!
:ok

Случай

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

> case [1,2,3] do
...> [1,2,3] -> IO.puts "Match!"
...> [4,2,3] -> IO.puts "No Match!"
...> [1,2,x] -> IO.puts "Match first two items and x would equal 3"
...> _ -> IO.puts "Wildcard: match any value"
...> [_,_,3] -> IO.puts "Match any value for the first two."
...> end
Match
:ok

Поскольку вы можете связывать элементы и использовать подстановочные знаки, это чрезвычайно полезно, но при этом прекрасно уплотняется.

Cond

Если вы хотите сопоставить условные выражения вместо значений, используйте cond:

> cond do 
...> 1 === 2 -> "LOL"
...> 1 === "pie" -> "It better be cherry!"
...> 1 === 1 -> "Squadron up!"
...> end
"Squadron up!"

Функции, модули и оператор конвейера

Последний раздел… у-у-у!

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

Мы уже обсуждали анонимные функции, которые выглядели так:

increase = fn(number) -> number + 1 end

Есть также сокращенный способ записать это:

increase = &(&1 + 1)

Первый & можно рассматривать как замену fn() ->. &1 относится к первому (и единственному) параметру, который мы хотим увеличить на 1 и вернуть. end также опущен.

Именованные функции и модули

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

defmodule Animals do
  def dog(name) do
    "Bark bark " <> name
  end
  def cat(name) do
    "Meow meow " <> name
  end
end

defmodule определяет модуль, который мы назвали Animals. В нем у нас есть две функции, dog и cat, которые выполняются с помощью def.

Чтобы вызвать наши функции, мы бы сделали:

> Animals.dog("Max")
"Bark bark Max"
> Animals.cat("Daisy")
"Meow meow Daisy"

Важное примечание: имена модулей должны быть заглавными, а имена функций - строчными.

Оператор трубы

В нашем коде у нас будет несколько функций. Будет много случаев, когда мы захотим взять результат одной функции и использовать его в качестве параметра для другой функции.

Вот как это можно было бы написать:

//HelloWorld.hello returns "Hello"
//myModule.world(param) returns param <> " World"
> HelloWorld.world(myModule.hello)
"Hello World"

Теперь этот простой пример не слишком сбивает с толку. Однако есть более простой способ записать это с помощью оператора вертикальной черты |>:

> HelloWorld.hello |> HelloWorld.world
"Hello World"

Вот полный пример:

Мы также можем использовать это, чтобы просто отделить параметры от функций:

> "hello" |> HelloWorld.world
> "Hello World"

Дополнительная практика

Это все что она написала! Мы завершили изучение основ Elixir. Надеюсь, это было весело и увлекательно. У нас должно быть все необходимое, чтобы окунуться в Phoenix, однако я бы посоветовал вам поиграть и сделать несколько упражнений.

Я рекомендую находить практические задачи Python и решать их с помощью Elixir.

Другие источники:

Школа Эликсира
Руководство по началу работы
Официальная документация
Программирование Эликсира

Удачного взлома! 💪

Следующий

В следующей главе мы познакомимся с концепциями и терминологией, лежащими в основе Phoenix, и вместе создадим наше первое приложение Phoenix.

Глава 3

Глава 3 теперь доступна.

Подпишитесь на уведомления

Получайте уведомления о выходе каждой главы.

С уважением,
Майк Манджаларди
Основатель Coding Artist