Предыдущие главы
«Глава 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
Подпишитесь на уведомления
Получайте уведомления о выходе каждой главы.
С уважением,
Майк Манджаларди
Основатель Coding Artist