diff --git a/modules/10-basics/10-hello-world/description.en.yml b/modules/10-basics/10-hello-world/description.en.yml deleted file mode 100644 index a81df0c..0000000 --- a/modules/10-basics/10-hello-world/description.en.yml +++ /dev/null @@ -1,31 +0,0 @@ ---- -name: Hello, World! -theory: | - - Elixir is a dynamic, functional programming language designed for building scalable and easily maintainable applications. - - Elixir was created by José Valim and appeared for the first time in 2011. It steadily gained popularity since then. Today some big companies use Elixir in production, including Brex, Heroku, Discord, Pinterest, and others. - - It runs on top of the Erlang virtual machine BEAM and inherits all the goodies of Erlang OTP. It's famous for its unique features for creating reliable distributed systems and extensively used in real-time embedded systems and apps: messengers, online games, etc. - - Elixir is a very pragmatic language that is easy to learn. Have fun! - -instructions: | - Type this code in the editor and hit "Run" - - ```elixir - # Define a module Solution - defmodule Solution do - # define a function hello - def hello do - # Call an other module's function - IO.puts("Hello, World!") # No colon - end - end - ``` - -tips: - - | - [repl.it](https://repl.it/languages/elixir) - write your Elixir experiments in your browser. - [Phoenix](https://phoenixframework.org/) - the most popular web framework in Elixir. - [Elixir: The Documentary](https://youtu.be/lxYFOM3UJzo) - the video that tells the origins of Elixir diff --git a/modules/10-basics/10-hello-world/description.ru.yml b/modules/10-basics/10-hello-world/description.ru.yml deleted file mode 100644 index a3f5d66..0000000 --- a/modules/10-basics/10-hello-world/description.ru.yml +++ /dev/null @@ -1,40 +0,0 @@ ---- - -name: Привет, Мир! -theory: | - - Elixir – динамический, функциональный язык программирования, спроектированный для создания масштабируемых и легко поддерживаемых приложений. Эликсир был выпущен в 2012 году Жозе Валимом, и несколько лет поддерживался только своим создателем. В какой-то момент его популярность выросла настолько, что многие компании начали серьезно использовать его в своих проектах. - - Эликсир работает поверх виртуальной машины языка эрланг. Она широко известна своими уникальными возможностями для создания отказоустойчивых и распределенных систем. Причем, как во встраиваемых устройствах, например роутерах, так и в вебе, при создании приложений реального времени (игры, мессенджеры). - - Практически все, что говорится про эликсир, является заслугой виртуальной машины эрланга. Эликсир задумывался как язык, который привносит в мир эрланга кое-что новое, чего не хватало самому эрлангу. В первую очередь это средства, повышающие уровень абстракции (Struct, Protocol), позволяющие писать более лаконичный код (оператор pipe, конструкция with), и удобно управлять проектом и его зависимостями (mix). Кроме того, это метапрограммирование -- мощная система макросов, позволяющая создавать DSL-языки. Хорошим примером такого DSL-языка является библиотека Ecto для работы с базами данных. - - Эликсир – практичный язык. Он прост в освоении и эффективен в работе. На его базе создан веб-фреймворк Phoenix, очень напоминающий упрощенный Ruby On Rails. Его код открыт и доступен на гитхабе. Было бы несправедливо не сделать обучение по Elixir именно тут) - -instructions: | - Наберите в редакторе код из задания символ в символ и нажмите «Проверить». - - ```elixir - # Определение модуля Solution - defmodule Solution do - # Определение функции hello - # Отступ 2 пробела - def hello do - # Вызов функции другого модуля - # Отступ 2 пробела - IO.puts("Hello, World!") # В конце не нужна точка с запятой - end - end - ``` - -tips: - - | - [repl.it](https://repl.it/languages/elixir) - здесь вы можете экспериментировать с кодом на Elixir. - - | - [Официальный сайт](https://elixir-lang.org/) - - | - [Phoenix, веб-фреймворк](https://phoenixframework.org/) - - | - [Исходный код языка](https://github.com/elixir-lang/elixir) - - | - [Официальный форум, посвященный языку](https://elixirforum.com/) diff --git a/modules/10-basics/10-hello-world/en/data.yml b/modules/10-basics/10-hello-world/en/data.yml index 42e13e4..07c955b 100644 --- a/modules/10-basics/10-hello-world/en/data.yml +++ b/modules/10-basics/10-hello-world/en/data.yml @@ -1,3 +1,4 @@ +--- name: Hello, World! tips: - > diff --git a/modules/10-basics/10-hello-world/ru/data.yml b/modules/10-basics/10-hello-world/ru/data.yml index 7a98ba5..085b1bb 100644 --- a/modules/10-basics/10-hello-world/ru/data.yml +++ b/modules/10-basics/10-hello-world/ru/data.yml @@ -1,3 +1,4 @@ +--- name: Привет, Мир! tips: - > diff --git a/modules/10-basics/20-modules/description.en.yml b/modules/10-basics/20-modules/description.en.yml deleted file mode 100644 index 5045304..0000000 --- a/modules/10-basics/20-modules/description.en.yml +++ /dev/null @@ -1,52 +0,0 @@ ---- -name: Modules -theory: | - - In Elixir, functions are grouped by modules that play the role of namespaces. For example, in the previous exercise we used the function `puts(text)` from module _IO_: - - ```elixir - IO.puts("Hello, World!") - ``` - - Modules are defined with a special contstruct _defmodule/do/end_: - - ```elixir - # use CamelCase for name - defmodule HexletBasics do - # Here goes module contains - end - ``` - - Elixir allows the creation of multiple modules in the same file, but there is a convention to have only one module per file with the same name as the module but in _snake_case_. - - Modules can be nested. By convention, we create nested modules when they are located in a directory. For example, there's the module `HexletBasics.Application` located in _lib/hexlet_basics/application.ex_ directory. - - ```elixir - defmodule HexletBasics.Application do - # Function - def hello do - IO.puts("Hello, World!") - end - end - ``` - - Function calls for nested modules are the same as for flat modules. First, we put the module's full name, then a function call. - - ```elixir - HexletBasics.Application.hello() - ``` - - There is a special module _Kernel_ which functions we can call directly. - - ```elixir - is_number(13) # true - ``` - - _Kernel_ contains basic language primitives for arithmetic operations, spawning processes, data type handling, macros for defining new functionality, and more. - -instructions: | - Create module _My.Super.Module_ and put the function `hello()` we defined above - -tips: - - | - [Kernel](https://hexdocs.pm/elixir/Kernel.html) diff --git a/modules/10-basics/20-modules/description.ru.yml b/modules/10-basics/20-modules/description.ru.yml deleted file mode 100644 index d171be7..0000000 --- a/modules/10-basics/20-modules/description.ru.yml +++ /dev/null @@ -1,55 +0,0 @@ ---- - -name: Модули -theory: | - - В эликсире функции группируются по модулям, которые выполняют роль неймспейсов. Например, в предыдущем уроке мы использовали функцию `puts(text)` из модуля _IO_: - - ```elixir - # Обращение по имени не зависит от того, в каком месте программы происходит вызов - IO.puts("Hello, World!") - ``` - - Модули определяются с помощью конструкции _defmodule/do/end_: - - ```elixir - # Имя записывается в CamelCase - defmodule HexletBasics do - # Здесь описывается содержимое модуля - end - ``` - - Эликсир позволяет создавать несколько модулей в одном файле, но так делают не часто. Обычно, создают один модуль на один файл. Имя этого файла получается из имени модуля переводом его _CamelCase_ в _snake_case_. - - Модули могут быть вложенными. Как правило, их помещают внутри директорий относительно базовой директории проекта. Например, в коде этого сайта есть модуль `HexletBasics.Application`, который расположен в директории _lib/hexlet_basics/application.ex_. - - ```elixir - defmodule HexletBasics.Application do - # Функция - def hello do - IO.puts("Hello, World!") - end - end - ``` - - Обращение к функциям вложенных модулей ничем не отличается от обращения к плоским модулям. Сначала указывается полное имя модуля, за которым идет вызов функции: - - ```elixir - HexletBasics.Application.hello() - ``` - - Один модуль встроенный в Эликсир является особенным. Это модуль _Kernel_. Функции этого модуля можно вызывать напрямую, без указания самого модуля: - - ```elixir - is_number(13) # true - ``` - - _Kernel_ содержит базовые языковые примитивы для арифметических операций, порождения процессов, определения новых функций и модулей, обработки типов данных и так далее. - -instructions: | - - Определите модуль _My.Super.Module_ и поместите в него функцию `hello()` описанную выше. - -tips: - - | - [Kernel](https://hexdocs.pm/elixir/Kernel.html) diff --git a/modules/10-basics/20-modules/en/data.yml b/modules/10-basics/20-modules/en/data.yml index 3078f44..7fae065 100644 --- a/modules/10-basics/20-modules/en/data.yml +++ b/modules/10-basics/20-modules/en/data.yml @@ -1,3 +1,4 @@ +--- name: Modules tips: - | diff --git a/modules/10-basics/20-modules/ru/data.yml b/modules/10-basics/20-modules/ru/data.yml index add5a46..f54e5dd 100644 --- a/modules/10-basics/20-modules/ru/data.yml +++ b/modules/10-basics/20-modules/ru/data.yml @@ -1,3 +1,4 @@ +--- name: Модули tips: - | diff --git a/modules/10-basics/25-functions/description.en.yml b/modules/10-basics/25-functions/description.en.yml deleted file mode 100644 index d1bf0ff..0000000 --- a/modules/10-basics/25-functions/description.en.yml +++ /dev/null @@ -1,68 +0,0 @@ ---- -name: Functions -theory: | - - At a glance, functions in Elixir are like ones in imperative languages. But that only seems true. They are different in many ways and that influences your code organization inside them a lot. - - The easiest way to define a function is to use the construction _def/do/end_: - - ```elixir - def hello do - IO.puts("Hello, World!") - end - ``` - - If a function accepts no arguments, then we can omit brackets. - - With arguments we write it the same way most of the languages has it: - - ```elixir - defmodule Math do - # Write name in snake_case - def sum_of_values(a, b) do - # Basic sum - a + b - end - end - - # We can call functions without brackets! - IO.puts Math.sum_of_values(5, 8) # => 13 - ``` - - In Elixir there's no instruction `return` that might stop code execution. The result of function execution is always the last expression result. If the function is empty then it returns _nil_ which is equal to _null_ in other languages. - - Another feature is that we can call a function without brackets. But there is a trick: in nested calls, it might cause confusion and unexpected behavior: `IO.puts IO.puts "huh"`. - - For one-liners Elixir supports a special short declaration syntax: - - ```elixir - # Note that we put a comma before _do_ and we omit _end_ - def sum_of_values(a, b), do: a + b - - # or - - def hello, do: IO.puts("Hello, World!") - ``` - - Functions defined with `def` are available outside of its module. If we want to make a private function we should define it using `defp`. - - ```elixir - defmodule Solution do - # We can call the same module functions without specifying its module - def hello, do: IO.puts(text_for_hello()) - - defp text_for_hello, do: "Hello, World" - end - ``` - -instructions: | - - Create function `print_twice(value)` that prints provided value twice - - ```elixir - Solution.print_twice("WoW") - # => WoW - # => WoW - ``` - -tips: [] diff --git a/modules/10-basics/25-functions/description.ru.yml b/modules/10-basics/25-functions/description.ru.yml deleted file mode 100644 index 6b7378f..0000000 --- a/modules/10-basics/25-functions/description.ru.yml +++ /dev/null @@ -1,69 +0,0 @@ ---- - -name: Функции в Elixir -theory: | - - Функции в эликсире, на первый взгляд, похожи на функции обычных императивных языков. Но это только на первый. Как вы увидите дальше, они во многом устроены по другому, что сильно влияет на способ организации кода внутри них. - - Самый простой способ определить функцию – использовать конструкцию _def/do/end_: - - ```elixir - def hello do - IO.puts("Hello, World!") - end - ``` - - Если функция не принимает аргументов, то при ее определении можно не указывать скобок. - - Если функция принимает параметры, то они указываются в определении, так же как и в большинстве других языков: - - ```elixir - defmodule Math do - # Имя записывается в snake_case - def sum_of_values(a, b) do - # Обычное сложение - a + b - end - end - - # Функция может вызываться без скобок! - IO.puts Math.sum_of_values(5, 8) # => 13 - ``` - - В Эликсире нет инструкции `return`, которая могла бы прервать выполнение кода. Результатом выполнения функции всегда является значение последнего вычисленного выражения. Если функция пустая, то возвращается _nil_, который аналогичен _null_ в других языках. - - Другая особенность, функции можно вызывать без скобок. Раньше такой синтаксис был популярным, но сейчас не принято так делать. Во вложенных вызовах такая запись становится не очевидной: `IO.puts inspect "huh"`. - - Для коротких функций, состоящих из одного выражения, Эликсир поддерживает специальный, сокращенный синтаксис объявления: - - ```elixir - # Обратите внимание на запятую после описания сигнатуры функции и отсутствие end в конце - def sum_of_values(a, b), do: a + b - - # или - - def hello, do: IO.puts("Hello, World!") - ``` - - Функции определяемые с помощью `def` доступны для вызова снаружи модуля. Но не все функции создаваемые внутри модуля, создаются для вызовов снаружи. Иногда функции выполняют вспомогательную роль для других функций этого же модуля. В такой ситуации лучше определять функцию через `defp`. Эта конструкция делает функцию приватной, то есть ее становится невозможно вызвать за пределами текущего модуля. - - ```elixir - defmodule Solution do - # К функциям из того же модуля можно обращаться без указания модуля - def hello, do: IO.puts(text_for_hello()) - - defp text_for_hello, do: "Hello, World" - end - ``` - -instructions: | - - Реализуйте функцию `print_twice(value)`, которая печатает на экран переданное значение два раза - - ```elixir - Solution.print_twice("WoW") - # => WoW - # => WoW - ``` - -tips: [] diff --git a/modules/10-basics/25-functions/en/data.yml b/modules/10-basics/25-functions/en/data.yml index 6b7cc82..8c93c2b 100644 --- a/modules/10-basics/25-functions/en/data.yml +++ b/modules/10-basics/25-functions/en/data.yml @@ -1,2 +1,3 @@ +--- name: Functions tips: [] diff --git a/modules/10-basics/25-functions/ru/data.yml b/modules/10-basics/25-functions/ru/data.yml index 3c3a9ce..8a90e97 100644 --- a/modules/10-basics/25-functions/ru/data.yml +++ b/modules/10-basics/25-functions/ru/data.yml @@ -1,2 +1,3 @@ +--- name: Функции в Elixir tips: [] diff --git a/modules/10-basics/30-strings/description.en.yml b/modules/10-basics/30-strings/description.en.yml deleted file mode 100644 index 3ad4e0e..0000000 --- a/modules/10-basics/30-strings/description.en.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- -name: Strings -theory: | - - 🚧 In development - -instructions: | - - 🚧 In development - -tips: [] diff --git a/modules/10-basics/30-strings/description.ru.yml b/modules/10-basics/30-strings/description.ru.yml deleted file mode 100644 index 40c1552..0000000 --- a/modules/10-basics/30-strings/description.ru.yml +++ /dev/null @@ -1,44 +0,0 @@ ---- - -name: Строки -theory: | - - В эликсир есть два вида строк. Строки в одинарных кавычках представляют собой последовательность Unicode символов, где каждый символ представлен 32-х разрядным числом. То есть, он занимает 4 байта. Такие строки используются нечасто. - - Гораздо чаще используются строки в двойных кавычках, которые представляют собой бинарные данные в формате UTF-8. В этом формате символы латинского алфавита занимают 1 байт, символы кириллицы занимают 2 байта, а символы других алфавитов занимают от 1 до 4-х байт. То есть, такая строка занимает значительно меньше памяти. - - Для склеивания двух строк применяется оператор `<>`: - - ```elixir - "Hello" <> " " <> "World!" # "Hello World!" - ``` - - Модуль `String` из стандартной библиотеки содержит функции для работы со строками. Например, функцию для определения длины строки `String.length(my_str)`, функцию для разбиения строки на части `String.split(my_str, separator)` функцию для удаления пробельных символов `String.trim(my_str)` и еще несколько десятков функций. - - Одна из операций над строками, которая на первый взгляд кажется простой, это перевод строки в верхний или нижний регистр. На самом деле для некоторых алфавитов эта операция должна учитывать контекст, а не просто один символ. А для некоторых других алфавитов она не имеет смысла. - - Функция `String.upcase(str, mode)` работает в трех разных режимах. В режиме `:default` она переводит в верхний регистр все символы, для которых это возможно. В режиме `:ascii` она переводит только символы латинского алфавита: - - ```elixir - String.upcase("hello мир!", :default) # "HELLO МИР!" - String.upcase("hello мир!", :ascii) # "HELLO мир!" - ``` - - Третий режим -- `:greek` используется для греческого алфавита, где, как раз, эта операция зависит от контекста. - - -instructions: | - - Реализуйте функцию `join_and_upcase`, которая принимает две строки, соединяет их вместе, удаляет пробелы в начале и в конце, и переводит в верхний регистр только символы латинского алфавита. - - ```elixir - Solution.join_and_upcase(" привет ", "world!") - # => "привет WORLD!" - - Solution.join_and_upcase("hello ", "мир! ") - # => "HELLO мир!" - ``` - -tips: - - | - [String.upcase/2](https://hexdocs.pm/elixir/String.html#upcase/2) diff --git a/modules/10-basics/30-strings/en/data.yml b/modules/10-basics/30-strings/en/data.yml index c375f97..4cd42b4 100644 --- a/modules/10-basics/30-strings/en/data.yml +++ b/modules/10-basics/30-strings/en/data.yml @@ -1,2 +1,3 @@ +--- name: Strings tips: [] diff --git a/modules/10-basics/30-strings/ru/data.yml b/modules/10-basics/30-strings/ru/data.yml index c87b786..07f9a3c 100644 --- a/modules/10-basics/30-strings/ru/data.yml +++ b/modules/10-basics/30-strings/ru/data.yml @@ -1,3 +1,4 @@ +--- name: Строки tips: - | diff --git a/modules/10-basics/40-numbers/description.en.yml b/modules/10-basics/40-numbers/description.en.yml deleted file mode 100644 index 8a53a6a..0000000 --- a/modules/10-basics/40-numbers/description.en.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- -name: Numbers -theory: | - - 🚧 In development - -instructions: | - - 🚧 In development - -tips: [] diff --git a/modules/10-basics/40-numbers/description.ru.yml b/modules/10-basics/40-numbers/description.ru.yml deleted file mode 100644 index 9a47729..0000000 --- a/modules/10-basics/40-numbers/description.ru.yml +++ /dev/null @@ -1,84 +0,0 @@ ---- - -name: Арифметика -theory: | - - В Эликсир есть два вида чисел — целые и с плавающей точкой. - - Целые числа могут быть представлены разными способами. В разных системах исчисления: в десятичной, шестнадцатеричной, восьмеричной и двоичной: - - ```elixir - 42 - 0x2A - 0o52 - 0b101010 - ``` - - В экспоненциальном виде: - - ```elixir - 0.42e2 - ``` - - Для больших чисел можно использовать символ подчеркивания между разрядами для удобства чтения: - - ```elixir - 100_500 - 1_000_000 - ``` - - Числа с плавающей точкой реализованы по стандарту IEEE 754, как и в большинстве других языков. Это значит, что их точность ограничена, и при некоторых операциях возможна потеря точности: - - ```elixir - 0.1 + 0.2 # 0.30000000000000004 - ``` - - Для целых чисел, и чисел с плавающей точкой реализованы обычные арифметические операции: - - ```elixir - 20 + 22 # 42 - 20.0 + 22 # 42.0 - 50 - 8.0 # 42.0 - 2 * 16 # 32 - 4 * 16.0 # 64.0 - 128 / 2 # 64.0 - 64.0 / 4.0 # 16.0 - ``` - - Операторы `+ - *` возвращают целое число, если оба аргумента целые, и число с плавающей точкой, если хотя бы один из аргументов с плавающей точкой. Оператор `/` всегда возвращает число с плавающей точкой. - - Еще есть оператор целочисленного деления `div` и оператор взятия остатка `rem`: - - ```elixir - div(42, 2) # 21 - div(45, 2) # 22 - rem(42, 2) # 0 - rem(45, 2) # 1 - ``` - -instructions: | - - Реализуйте функцию `do_math(a, b)`, которая принимает два числа, и выводит на экран: - - * результат деления суммы первого и второго числа на второе число - * результат целочисленного деления первого числа на второе - * остаток от деления второго числа на первое - - Каждый результат выводится на отдельной строке. - - ```elixir - Solution.do_math(10, 10) - # => 2.0 - # => 1 - # => 0 - - Solution.do_math(42, 3) - # => 15.0 - # => 14 - # => 3 - ``` - - -tips: - - | - Обратите внимание, в каком порядке требуется использовать числа. diff --git a/modules/10-basics/40-numbers/en/data.yml b/modules/10-basics/40-numbers/en/data.yml index efe6c6a..23ed8f3 100644 --- a/modules/10-basics/40-numbers/en/data.yml +++ b/modules/10-basics/40-numbers/en/data.yml @@ -1,2 +1,3 @@ +--- name: Numbers tips: [] diff --git a/modules/10-basics/40-numbers/ru/data.yml b/modules/10-basics/40-numbers/ru/data.yml index 0b55855..3987745 100644 --- a/modules/10-basics/40-numbers/ru/data.yml +++ b/modules/10-basics/40-numbers/ru/data.yml @@ -1,3 +1,4 @@ +--- name: Арифметика tips: - | diff --git a/modules/10-basics/50-booleans/description.en.yml b/modules/10-basics/50-booleans/description.en.yml deleted file mode 100644 index 608adec..0000000 --- a/modules/10-basics/50-booleans/description.en.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- -name: Boolean and logic -theory: | - - 🚧 In development - -instructions: | - - 🚧 In development - -tips: [] diff --git a/modules/10-basics/50-booleans/description.ru.yml b/modules/10-basics/50-booleans/description.ru.yml deleted file mode 100644 index 2e09eee..0000000 --- a/modules/10-basics/50-booleans/description.ru.yml +++ /dev/null @@ -1,99 +0,0 @@ ---- - -name: Логика -theory: | - - Булевый тип в Эликсир представлен значениями `true` и `false`. Но еще в языке есть `nil` — специальное значение выражающее отсутствие информации. - - Двоичная логика представлена операторами `and`, `or`, `not`. Это "строгие" операторы, они работают только со значениями `true` и `false`: - - ```elixir - true and true # true - true and false # false - true or true # true - true or false # true - not true # false - not false # true - ``` - - Так же есть операторы `&&`, `||`, `!`. Их называют "смягченными" (relaxed) операторами, потому что в отличие от строгих, они принимают любые значения. При этом все значения, кроме `false` и `nil` интерпретируются как `true`: - - ```elixir - 42 && true # true - false && nil # false - true || 42 # true - true || nil # true - ! nil # true - ! 42 # false - ``` - - Оператор `&&` в успешном случае возвращает свой второй аргумент, в неуспешном случае возвращает неуспешный аргумент: - - ```elixir - true && 42 # успешно, второй аргумент, 42 - 42 && true # успешно, второй аргумент, true - false && 42 # не успешно, false - nil && 42 # не успешно, nil - 42 && nil # не успешно, nil - ``` - - Есть еще варианты: - - ```elixir - false && nil # false - nil && false # nil - ``` - - Но можно не запоминать их, так как на практике это не так важно. - - Оператор `||` который проверяет и возвращает второе выражение только в том случае, если первое не имеет истинностного значения (то есть либо равно nil, либо равно false). В противном случае возвращается первое выражение: - - ```elixir - 42 || false # 42 - false || 42 # 42 - nil || false # false - false || nil # nil - ``` - - Оператор `!` возвращает `true` или `false` при любых аргументах: - - ```elixir - ! 42 # false - ! true # false - ! false # true - ! nil # true - ``` - - В английской терминологии в контексте этих операторов значения `false` и `nil` называют *falsy*, а все остальные значения называют *truthy*. В буквальном переводе это значит "фальшивый" и "правдивый". - - И строгие, и смягченные операторы являются ленивыми. То есть, они вычисляют только часть выражения, если этого достаточно: - - ```elixir - IO.puts("a") && IO.puts("b") # => a b - IO.puts("a") || IO.puts("b") # => a - true or IO.puts("b") # true - false and IO.puts("b") # false - ``` - - Некоторые разработчики используют их для вывода сообщения об ошибке в случае неуспешной операции: - - ```elixir - do_something() || IO.puts("error") - ``` - - -instructions: | - - Реализуйте функцию `any?(a, b, c, d)`, которая принимает четыре булевых аргумента, и возвращает `true`, если среди аргументов есть `true`. - - Реализуйте функцию `truthy?(a, b)`, которая принимает два аргумента любого типа, и если первый аргумент truthy, то функция возвращает второй аргумент. - - ```elixir - Solution.any?(false, false, false, false) # => false - Solution.any?(true, false, false, false) # => true - Solution.any?(false, true, false, true) # => true - - Solution.truthy?(true, 42) # => 42 - Solution.truthy?("hello", false) # => false - Solution.truthy?("", nil) # => nil - ``` diff --git a/modules/10-basics/50-booleans/en/data.yml b/modules/10-basics/50-booleans/en/data.yml index 71ee026..6980240 100644 --- a/modules/10-basics/50-booleans/en/data.yml +++ b/modules/10-basics/50-booleans/en/data.yml @@ -1,2 +1,3 @@ +--- name: Boolean and logic tips: [] diff --git a/modules/10-basics/50-booleans/ru/data.yml b/modules/10-basics/50-booleans/ru/data.yml index 9a66b91..c0b76da 100644 --- a/modules/10-basics/50-booleans/ru/data.yml +++ b/modules/10-basics/50-booleans/ru/data.yml @@ -1 +1,2 @@ +--- name: Логика diff --git a/modules/10-basics/60-attributes/description.en.yml b/modules/10-basics/60-attributes/description.en.yml deleted file mode 100644 index 21038f4..0000000 --- a/modules/10-basics/60-attributes/description.en.yml +++ /dev/null @@ -1,111 +0,0 @@ ---- -name: Attributes -theory: | - - Sometimes, in a module, you need to put some values into constants, for this use attributes starting with `@` sign. - - ```elixir - defmodule MyModule do - @magic_number 8 - - def do_magic(num) do - num * @magic_number - end - end - - MyModule.do_magic(10) # 80 - ``` - - Module attributes are not available from outside the module. - - ```elixir - defmodule MyModule do - @magic_number 8 - end - - MyModule.@magic_number # raises error - ``` - - Attributes can be declared several times and redefined, but it should be noted that the compiler inlines the last value of the declared attribute, for example: - - ```elixir - defmodule MyModule do - @magic_number 8 - - def cast_magic() do - @magic_number - end - - @magic_number 0 - - def do_magic() do - @magic_number - end - end - - MyModule.cast_magic() # 8 - - MyModule.do_magic() # 0 - - # after compilation module attributes inlines like this - defmodule MyModule do - @magic_number 8 - - def cast_magic() do - 8 - end - - @magic_number 0 - - def do_magic() do - 0 - end - end - ``` - - Within modules, there are also special attributes that are used by Elixir to generate documentation, such as the `@moduledoc` attribute that describes general information about the module or the `@doc` attribute that documents a declared function: - - ```elixir - defmodule MyModule do - @moduledoc "My attributes exercise module." - - @magic_number 8 - - @doc "Do some magic calculations." - def do_magic(num) do - num * @magic_number - end - end - ``` - - Then, these attributes are used in documentation generation. - - You can also make a module attribute accumulate redefined values, with a special attribute declaration, for example: - - ```elixir - defmodule MyModule do - Module.register_attribute __MODULE__, :magic_values, accumulate: true - - @magic_values 8 - @magic_values :some - @magic_values "hello" - - def do_magic() do - @magic_values - end - end - - MyModule.do_magic # [8, :some, "hello"] - ``` - -instructions: | - Define the following attributes with values inside the module: - - Attribute: number_attr, value: 10 - - Attribute: boolean_attr, value: false - - Attribute: hello_world_attr, value: "Hello, World!" - -tips: - - | - [Library documentation example](https://github.com/michalmuskala/jason/blob/v1.4.0/lib/jason.ex) - - | - [Official documentation](https://elixir-lang.org/getting-started/module-attributes.html) diff --git a/modules/10-basics/60-attributes/description.ru.yml b/modules/10-basics/60-attributes/description.ru.yml deleted file mode 100644 index bdfd10f..0000000 --- a/modules/10-basics/60-attributes/description.ru.yml +++ /dev/null @@ -1,111 +0,0 @@ ---- -name: Атрибуты -theory: | - - Иногда в модуле нужно вынести значения в константы. Для этого используются атрибуты, которые начинаются со знака `@`: - - ```elixir - defmodule MyModule do - @magic_number 8 - - def do_magic(num) do - num * @magic_number - end - end - - MyModule.do_magic(10) # 80 - ``` - - Атрибуты модуля недоступны снаружи модуля: - - ```elixir - defmodule MyModule do - @magic_number 8 - end - - MyModule.@magic_number # возникнет ошибка - ``` - - Атрибуты можно объявлять несколько раз и перезаписывать. При этом стоит понимать, что компилятор подставляет последнее значение объявленного атрибута, например: - - ```elixir - defmodule MyModule do - @magic_number 8 - - def cast_magic() do - @magic_number - end - - @magic_number 0 - - def do_magic() do - @magic_number - end - end - - MyModule.cast_magic() # 8 - - MyModule.do_magic() # 0 - - # по сути после компиляции модуль будет выглядеть так - defmodule MyModule do - @magic_number 8 - - def cast_magic() do - 8 - end - - @magic_number 0 - - def do_magic() do - 0 - end - end - ``` - - Еще внутри модулей есть специальные атрибуты, которые используются эликсиром для генерации документации. Например, атрибут `@moduledoc`, в котором описывается общая информация о модуле, или `@doc`, которым документируется объявленная функция: - - ```elixir - defmodule MyModule do - @moduledoc "My attributes exercise module." - - @magic_number 8 - - @doc "Do some magic calculations." - def do_magic(num) do - num * @magic_number - end - end - ``` - - Затем эти атрибуты используются в генерации документации. - - Еще с помощью специального объявления можно заставить атрибут модуля при перезаписи накапливаться, например: - - ```elixir - defmodule MyModule do - Module.register_attribute __MODULE__, :magic_values, accumulate: true - - @magic_values 8 - @magic_values :some - @magic_values "hello" - - def do_magic() do - @magic_values - end - end - - MyModule.do_magic # [8, :some, "hello"] - ``` - -instructions: | - Определите внутри модуля следующие атрибуты со значениями: - - Атрибут: number_attr, значение: 10 - - Атрибут: boolean_attr, значение: false - - Атрибут: hello_world_attr, значение: "Hello, World!" - -tips: - - | - [Пример с документацией библиотеки](https://github.com/michalmuskala/jason/blob/v1.4.0/lib/jason.ex) - - | - [Официальная документация](https://elixir-lang.org/getting-started/module-attributes.html) diff --git a/modules/10-basics/60-attributes/en/data.yml b/modules/10-basics/60-attributes/en/data.yml index ec87e6e..75048fc 100644 --- a/modules/10-basics/60-attributes/en/data.yml +++ b/modules/10-basics/60-attributes/en/data.yml @@ -1,3 +1,4 @@ +--- name: Attributes tips: - > diff --git a/modules/10-basics/60-attributes/ru/data.yml b/modules/10-basics/60-attributes/ru/data.yml index 930aa8c..1e61a69 100644 --- a/modules/10-basics/60-attributes/ru/data.yml +++ b/modules/10-basics/60-attributes/ru/data.yml @@ -1,3 +1,4 @@ +--- name: Атрибуты tips: - > diff --git a/modules/10-basics/70-interop/description.en.yml b/modules/10-basics/70-interop/description.en.yml deleted file mode 100644 index 53a1695..0000000 --- a/modules/10-basics/70-interop/description.en.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- -name: Erlang interop -theory: | - - 🚧 In development - -instructions: | - - 🚧 In development - -tips: [] diff --git a/modules/10-basics/70-interop/description.ru.yml b/modules/10-basics/70-interop/description.ru.yml deleted file mode 100644 index af08e53..0000000 --- a/modules/10-basics/70-interop/description.ru.yml +++ /dev/null @@ -1,126 +0,0 @@ ---- -name: Интероп с Erlang -theory: | - - Так как Elixir имеет доступ к виртуальной машине Эрланг (Erlang VM), то это позволяет использовать все библиотеки, включая стандартную библиотеку Erlang, в коде на Elixir. - - Модули Erlang представлены атомами в нижнем регистре, такими как `:os` и `:timer`. Атомы - специальные строки, которые начинаются с символа `:`. Тему атомов подробнее рассмотрим в следующем модуле, а пока вернемся к примерам вызова Erlang кода из Elixir: - - ```elixir - # функция которая переводит секунды в миллисекунды - :timer.seconds(5) - # => 5000 - - :os.cmd(~c"whoami") - # => ~c"root\n" - - :os.getenv(~c"SHELL") - # => ~c"/bin/zsh" - ``` - - Для подключения пакетов Erlang достаточно указать их в mix.exs: - - ```elixir - # в mix.exs - def deps do - [{:png, github: "yuce/png"}] - end - - # теперь мы можем пользоваться этой библиотекой - png = - :png.create(%{:size => {30, 30}, :mode => {:indexed, 8}, :file => file, :palette => palette}) - ``` - - Однако есть и некоторые различия, например в атомах: - - ```elixir - # в эликсире - :my_atom - ``` - - ```erlang - # в эрланге - my_atom. - ``` - - Когда мы говорим о строках в Elixir, имеются ввиду бинарные объекты в UTF-8. В Erlang строки тоже используют двойные кавычки, но являются списком символов: - - ```elixir - # в эликсире - is_list('Example') # => true - is_list("Example") # => false - is_binary("Example") # => true - <<"Example">> === "Example" # => true - ``` - - ```erlang - # в эрланге - is_list('Example'). # => false - is_list("Example"). # => true - is_binary("Example"). # => false - is_binary(<<"Example">>). # => true - ``` - - Многие старые библиотеки Erlang могут не поддерживать бинарные строки, потому нужно превращать строки Elixir в строковые списки. Это достигается функцией `to_charlist` или с помощью сигиля `~c`. Сигили - особенные метки, которые выполняют преобразования над строками, подробнее их рассмотрим в следующем упражнении, а пока вернемся к примерам: - - ```elixir - # функция подсчитывающее количество слов из erlang - :string.words("Hello World") - # => ** (FunctionClauseError) no function clause matching in :string.strip_left/2 - # => - # => The following arguments were given to :string.strip_left/2: - # => - # => # 1 - # => "Hello World" - # => - # => # 2 - # => 32 - # => - # => (stdlib) string.erl:1661: :string.strip_left/2 - # => (stdlib) string.erl:1659: :string.strip/3 - # => (stdlib) string.erl:1597: :string.words/2 - - words = to_charlist("Hello World") - :string.words(words) # => 2 - - # или с помощью сигиля - :string.words(~c"Hello World") # => 2 - ``` - - В Erlang переменные начинаются с заглавной буквы, и повторная привязка (присваивание) не допускается: - - ```elixir - # в эликсире - x = 10 - x = 20 - x1 = x + 10 # => 30 - ``` - - ```erlang - # в эрланге - X = 10. - X = 20. - # ** exception error: no match of right hand side value 20 - X1 = X + 10. # => 20 - ``` - -instructions: | - - Создайте функцию `hours_to_milliseconds` которая конвертирует часы в миллисекунды, обратите внимание на `:timer` библиотеку из Erlang: - - ```elixir - Solution.hours_to_milliseconds(0) - # => 0 - Solution.hours_to_milliseconds(1.5) - # => 5400000.0 - Solution.hours_to_milliseconds(2) - # => 7200000 - ``` - -tips: - - | - [Документация на timer](https://www.erlang.org/doc/man/timer.html) - - | - [Документация на os](https://www.erlang.org/doc/man/os.html) - - | - [Официальный сайт Erlang](https://www.erlang.org/) diff --git a/modules/10-basics/70-interop/en/data.yml b/modules/10-basics/70-interop/en/data.yml index d6a3a7d..d729395 100644 --- a/modules/10-basics/70-interop/en/data.yml +++ b/modules/10-basics/70-interop/en/data.yml @@ -1,2 +1,3 @@ +--- name: Erlang interop tips: [] diff --git a/modules/10-basics/70-interop/ru/data.yml b/modules/10-basics/70-interop/ru/data.yml index 5ff06b6..7213089 100644 --- a/modules/10-basics/70-interop/ru/data.yml +++ b/modules/10-basics/70-interop/ru/data.yml @@ -1,3 +1,4 @@ +--- name: Интероп с Erlang tips: - | diff --git a/modules/10-basics/75-sigils/description.en.yml b/modules/10-basics/75-sigils/description.en.yml deleted file mode 100644 index a9ea79b..0000000 --- a/modules/10-basics/75-sigils/description.en.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- -name: Sigils -theory: | - - 🚧 In development - -instructions: | - - 🚧 In development - -tips: [] diff --git a/modules/10-basics/75-sigils/description.ru.yml b/modules/10-basics/75-sigils/description.ru.yml deleted file mode 100644 index d03ea84..0000000 --- a/modules/10-basics/75-sigils/description.ru.yml +++ /dev/null @@ -1,87 +0,0 @@ ---- -name: Строковые метки (сигили) -theory: | - - Помимо обычной обработки, Elixir имеет еще один механизм для взаимодействия со строками, называется он строковыми метками или сигилями. Благодаря им, можно упростить работу со специфичными для домена текстовыми данными. - - Классическим примером являются регулярные выражения: - - ```elixir - # это регулярное выражение проверяет вхождение слов foo или bar - regex = ~r/foo|bar/ - - "foo" =~ regex - # => true - "bat" =~ regex - # => false - ``` - - Помимо этого, в конце сигиля можно указать специальный модификатор, который будет влиять на его поведение. Например, если к регулярному выражению добавить модификатор `i`, то регулярное выражение будет *регистронезависимым*: - - ```elixir - "HELLO" =~ ~r/hello/ - # => false - - "HELLO" =~ ~r/hello/i - # => true - ``` - - Есть еще сигили, например `~w` для слов или `~s` для строк, в которых содержатся двойные кавычки: - - ```elixir - ~w(foo bar bat) - # => ["foo", "bar", "bat"] - - ~s(this is a string with "double" quotes, not 'single' ones) - # => "this is a string with \"double\" quotes, not 'single' ones" - ``` - - Помимо сигилей, доступных в самом языке, мы можем создавать свои, с помощью объявления функции `sigil_[x]`, где `x` - название сигиля. Создадим сигиль `~u` который переводит переданную строку в верхний регистр, для перевода воспользуемся функций `String.upcase`: - - ```elixir - defmodule MySigils do - def sigil_u(string, []), do: String.upcase(string) - end - - # для работы с новыми сигилями, нужно импортировать модуль, где они объявлены - import MySigils - - ~u(code basics) - # => CODE BASICS - ``` - - Важно, что функция-сигиль принимает два аргумента, саму строку и список модификаторов. - - Теперь добавим к сигилю `~u` два модификатора `f` и `l`, которые возвращают первый или последний символ строки: - - ```elixir - defmodule MySigils do - def sigil_u(string, []), do: String.upcase(string) - def sigil_u(string, [?f]), do: String.first(String.upcase(string)) - def sigil_u(string, [?l]), do: String.last(String.upcase(string)) - end - - ~u(code basics)f - # => C - ~u(code basics)l - # => S - ``` - -instructions: | - Создайте сигиль `~i` который переводит переданную в него строку в целое число и если указан модификатор `n`, который переводит переданное строку-число в отрицательное: - - ```elixir - import Solution - - ~i(40) - # => 40 - - ~i(21)n - # => -21 - ``` - - Для перевода строки в число воспользуйтесь функцией `to_integer` из модуля `String`. - -tips: - - | - [Официальная документация](https://hexdocs.pm/elixir/syntax-reference.html#sigils) diff --git a/modules/10-basics/75-sigils/en/data.yml b/modules/10-basics/75-sigils/en/data.yml index 835eb20..23e2e9f 100644 --- a/modules/10-basics/75-sigils/en/data.yml +++ b/modules/10-basics/75-sigils/en/data.yml @@ -1,2 +1,3 @@ +--- name: Sigils tips: [] diff --git a/modules/10-basics/75-sigils/ru/data.yml b/modules/10-basics/75-sigils/ru/data.yml index 3669ff5..e4b4408 100644 --- a/modules/10-basics/75-sigils/ru/data.yml +++ b/modules/10-basics/75-sigils/ru/data.yml @@ -1,3 +1,4 @@ +--- name: Строковые метки (сигили) tips: - > diff --git a/modules/20-data-types/10-atoms-and-tuples/description.en.yml b/modules/20-data-types/10-atoms-and-tuples/description.en.yml deleted file mode 100644 index b5c6d54..0000000 --- a/modules/20-data-types/10-atoms-and-tuples/description.en.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- -name: Atoms -theory: | - - In development - -instructions: | - - In development - -tips: [] diff --git a/modules/20-data-types/10-atoms-and-tuples/description.ru.yml b/modules/20-data-types/10-atoms-and-tuples/description.ru.yml deleted file mode 100644 index acfac86..0000000 --- a/modules/20-data-types/10-atoms-and-tuples/description.ru.yml +++ /dev/null @@ -1,120 +0,0 @@ ---- - -name: Атомы и кортежи -theory: | - - *Атомы* типичны для функциональных языков, но редко встречаются в языках императивных. Это некие константные значения, которые можно сравнивать друг с другом. Собственно, сравнивать — это единственное, что с ними можно делать. Сами по себе они не очень полезны, но становятся полезны в комплекте с кортежами и сопоставлением с образцом (pattern matching). - - ```elixir - :user - :point - :ip_address - ``` - - *Кортеж (tuple)* — это структура данных, объединяющая несколько разных значений. Кортеж похож на список, но в отличие от списка имеет фиксированную длину. - - ```elixir - {"Bob", :male, 23} - {1, 2} - {127, 0, 0, 1} - ``` - - В кортежах на первой позиции часто ставят атом, чтобы обозначить, что за данные собраны в кортеже. Таким образом кортеж помечается тэгом (tagged tuple). - - ```elixir - {:user, "Bob", :male, 23} - {:point, 1, 2} - {:ip_address, 127, 0, 0, 1} - ``` - - Кортежи могут быть вложенными: - - ```elixir - {:rectangle, {:point, 0, 0}, {:point, 10, 10}} - {:ip4, {127, 0, 0, 1}} - ``` - - Небольшие объекты, состоящие из 2-4 полей, удобно представлять в виде кортежей, если роль полей понятна из контекста. В ином случае нужно использовать *словарь (map)* или *структуру (struct)*. - - Атомы и кортежи — это легковесные объекты, они используют меньше памяти, чем словари и структуры, и операции над ними выполняются быстрее. - - Сопоставление с образцом мы будем изучать подробно. Сейчас важно знать, что это способ извлечь отдельные значения из кортежа: - - ```elixir - my_point = {:point, 5, 10} - {:point, x, y} = my_point - IO.puts(x) # => 5 - IO.puts(y) # => 10 - ``` - - Рассмотрим реализацию функции distance, которая вычисляет расстояние между двумя точками: - - ```elixir - def distance(point1, point2) do - {:point, x1, y1} = point1 - {:point, x2, y2} = point2 - x_dist = abs(x1 - x2) - y_dist = abs(y1 - y2) - :math.sqrt(:math.pow(x_dist, 2) + :math.pow(y_dist, 2)) - end - ``` - - Функция принимает в аргументах две точки, извлекает их координаты с помощью сопоставления с образцом, и по теореме Пифагора вычисляет расстояние между точками. - - Для этого применяется модуль *:math* из языка Эрланг, потому что у Эликсир нет своего такого модуля в стандартной библиотеке (есть в сторонних библиотеках). Если бы такой модуль был, то код выглядел бы так: - - ```elixir - Math.sqrt(Math.pow(x_dist, 2) + Math.pow(y_dist, 2)) - ``` - - Обычно извлечение значений из кортежа с помощью сопоставления с образцом делают прямо в аргументах функции: - - ```elixir - def distance({:point, x1, y1}, {:point, x2, y2}) do - x_dist = abs(x1 - x2) - y_dist = abs(y1 - y2) - :math.sqrt(:math.pow(x_dist, 2) + :math.pow(y_dist, 2)) - end - ``` - - Результат работы функции: - - ```elixir - distance({:point, 0, 0}, {:point, 0, 5}) # 5.0 - distance({:point, 2, 2}, {:point, 10, 12}) # 12.806248474865697 - distance({:point, -5, -5}, {:point, 10, 10}) # 21.213203435596427 - ``` - - При объявлении, атом сохраняется внутри памяти программы в виде числа, из-за чего сравнения атомов происходят быстрее, чем строк. Однако, если бесконтрольно объявлять атомы, например, из пользовательского ввода, то память приложения переполнится. Атомы не вычищаются сборщиком мусора BEAM. Хоть и максимальное число атомов, которые можно создать - 1,048,576, не стоит заводить атомы на каждое действие. - -instructions: | - - Реализовать функцию `is_point_inside_circle(point, circle)`, которая принимает точку и окружность, и возвращает `true`, если точка находится внутри окружности, или `false`, если точка находится снаружи. - - Реализовать функцию `is_point_inside_rect(point, rect)`, которая принимает точку и прямоугольник, и возвращает `true`, если точка находится внутри прямоугольника, или `false`, если точка находится снаружи. - - Точка представлена кортежем `{:point, x, y}`. - - Окружность представлена кортежем `{:circle, center, radius}`, где center — это кортеж `:point`. - - Прямоугольник представлен кортежем `{:rect, left_top, right_bottom}`, где `left_top` и `right_bottom` — это кортежи `:point`. - - Уже реализованная функция distance может вам пригодиться: - - ```elixir - Solution.is_point_inside_circle(point, {:circle, {:point, 10, 10}, 100}) - # => true - Solution.is_point_inside_circle(point, {:circle, {:point, -10, -10}, 20}) - # => false - - Solution.is_point_inside_rect(point, {:rect, {:point, -20, 30}, {:point, 20, 10}}) - # => true - Solution.is_point_inside_rect(point, {:rect, {:point, 0, 0}, {:point, 10, 10}}) - # => false - ``` - -tips: - - | - [Официальная документация](https://hexdocs.pm/elixir/Atom.html) - - | - [Почему максимальное число атомов 1,048,576](https://www.erlang.org/doc/man/erl.html#+t) diff --git a/modules/20-data-types/10-atoms-and-tuples/en/data.yml b/modules/20-data-types/10-atoms-and-tuples/en/data.yml index af0c8a8..7bd7c2f 100644 --- a/modules/20-data-types/10-atoms-and-tuples/en/data.yml +++ b/modules/20-data-types/10-atoms-and-tuples/en/data.yml @@ -1,2 +1,3 @@ +--- name: Atoms tips: [] diff --git a/modules/20-data-types/10-atoms-and-tuples/ru/data.yml b/modules/20-data-types/10-atoms-and-tuples/ru/data.yml index 94cd706..8d2f8a5 100644 --- a/modules/20-data-types/10-atoms-and-tuples/ru/data.yml +++ b/modules/20-data-types/10-atoms-and-tuples/ru/data.yml @@ -1,3 +1,4 @@ +--- name: Атомы и кортежи tips: - | diff --git a/modules/20-data-types/20-lists/description.en.yml b/modules/20-data-types/20-lists/description.en.yml deleted file mode 100644 index 867f2f2..0000000 --- a/modules/20-data-types/20-lists/description.en.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- -name: Lists -theory: | - - 🚧 In development - -instructions: | - - 🚧 In development - -tips: [] diff --git a/modules/20-data-types/20-lists/description.ru.yml b/modules/20-data-types/20-lists/description.ru.yml deleted file mode 100644 index 7b27f12..0000000 --- a/modules/20-data-types/20-lists/description.ru.yml +++ /dev/null @@ -1,68 +0,0 @@ ---- - -name: Списки -theory: | - - Список — коллекция элементов. В отличие от кортежа, в список можно динамически добавлять и удалять элементы. - - Список совсем не похож на массив ни по своему внутреннему устройству, ни по способу работы с ним. По внутреннему устройству это однонаправленный связанный список (singly linked list). А работа с ним сводится к двум простым операциям: добавление нового элемента к голове списка, и разделение списка на голову и хвост. - - Создадим список, и добавим в него новый элемент: - - ```elixir - my_list = [1, 2, 3, 4] # [1, 2, 3, 4] - other_list = [0 | my_list] # [0, 1, 2, 3, 4] - ``` - - Оператор `|` добавляет новый элемент к голове списка и возвращает новый список. - - Разделение списка на голову и хвост выглядит похоже: - - ```elixir - [head | tail] = my_list - IO.puts(head) # => 1 - IO.puts(inspect(tail)) # => [2, 3, 4] - ``` - - Здесь мы используем сопоставление с образцом. Шаблон (образец) `[head | tail]` сопоставляется со списком. Первый элемент списка попадает в переменную `head`, остаток списка (хвост) попадает в переменную `tail`. - - Можно извлечь несколько элементов одновременно: - - ```elixir - [item1, item2, item3 | tail] = my_list - IO.puts(item1) # => 1 - IO.puts(item2) # => 2 - IO.puts(item3) # => 3 - IO.puts(inspect(tail)) # => [4] - ``` - - Таким образом мы извлекли три элемента, и в списке остался только один. - - Есть еще функции `hd` и `tl`, которые извлекают голову и хвост списка: - - ```elixir - hd(my_list) # 1 - tl(my_list) # [2, 3, 4] - ``` - - Зачем нужны такие странные операции? На них основана итерация по элементам списка. А на итерации основана вообще любая работа со списками и с другими коллекциями. Это станет понятно позже, когда мы начнем изучать рекурсию и основы функционального программирования. - - -instructions: | - - Реализуйте функцию `get_second_item`, которая возвращает сумму первого и второго элементов списка. - - Внимательный читатель спросит: "а что функция должна сделать, если в списке только один элемент, или список вообще пустой?". На этот вопрос мы ответим в следующем модуле, где будем изучать ветвления в коде и сопоставление с образцом. Пока будем считать, что функция всегда вызывается со списком, содержащим два или больше элементов. - - ```elixir - Solution.get_second_item([20, 22, 24]) - # => 42 - Solution.get_second_item([1, 2, 3, 4]) - # => 3 - ``` - - Еще более внимательный читатель спросит: "а что, если список содержит элементы такого типа, для которого не определена операция суммирования?". В этом случае возникнет исключение. Исключения и обработку ошибок изучим в соответствующем модуле. - -tips: - - | - [Официальная документация](https://hexdocs.pm/elixir/List.html) diff --git a/modules/20-data-types/20-lists/en/data.yml b/modules/20-data-types/20-lists/en/data.yml index ccaa267..02ada50 100644 --- a/modules/20-data-types/20-lists/en/data.yml +++ b/modules/20-data-types/20-lists/en/data.yml @@ -1,2 +1,3 @@ +--- name: Lists tips: [] diff --git a/modules/20-data-types/20-lists/ru/data.yml b/modules/20-data-types/20-lists/ru/data.yml index 777d00e..5b0da9b 100644 --- a/modules/20-data-types/20-lists/ru/data.yml +++ b/modules/20-data-types/20-lists/ru/data.yml @@ -1,3 +1,4 @@ +--- name: Списки tips: - | diff --git a/modules/20-data-types/30-maps/description.en.yml b/modules/20-data-types/30-maps/description.en.yml deleted file mode 100644 index 64b80c0..0000000 --- a/modules/20-data-types/30-maps/description.en.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- -name: Maps -theory: | - - 🚧 In development - -instructions: | - - 🚧 In development - -tips: [] diff --git a/modules/20-data-types/30-maps/description.ru.yml b/modules/20-data-types/30-maps/description.ru.yml deleted file mode 100644 index e23153d..0000000 --- a/modules/20-data-types/30-maps/description.ru.yml +++ /dev/null @@ -1,127 +0,0 @@ ---- - -name: Словари -theory: | - - Словарь (Map) хранит пары ключ-значение. Это еще одна динамическая структура данных в которую можно добавлять и удалять элементы. - - Словарь создается с помощью конструкции `%{key1 => value1, key2 => value2}`. Для обращения по ключу используются квадратные скобки: - - ```elixir - my_map = %{"a" => 1, "b" => 2} - my_map["a"] # 1 - my_map["b"] # 2 - ``` - - Часто в качестве ключей используют атомы: - - ```elixir - other_map = %{:a => 1, :b => 2} - other_map[:a] # 1 - other_map[:b] # 2 - ``` - - Для ключей атомов (и только в этом случае) можно использовать синтаксический сахар: - - ```elixir - other_map = %{a: 1, b: 2} - other_map.a # 1 - other_map.b # 2 - ``` - - Обращение через квадратные скобки и обращение через точку работают по-разному в случае, когда ключ отсутствует в словаре: - - ```elixir - other_map[:c] # nil - other_map.c # ** (KeyError) key :c not found in: %{a: 1, b: 2} - ``` - - Как видим, в первом случае возвращается значение `nil`, а во втором случае генерируется исключение. - - Функция `Map.get` работает так же, как обращение через квадратные скобки. Но она позволяет указать дефолтное значение для отсутствующего ключа: - - ```elixir - Map.get(other_map, :a) # 1 - Map.get(other_map, :c) # nil - Map.get(other_map, :c, 42) # 42 - ``` - - Для добавления нового ключа или для изменения значения существующего ключа используется функция `Map.put`: - - ```elixir - Map.put(other_map, :c, 42) # %{a: 1, b: 2, c: 42} - Map.put(other_map, :a, 42) # %{a: 42, b: 2} - ``` - - Словарь, как и все остальные значения в Эликсир, является иммутабельным. Поэтому функция `Map.put` возвращает новый словарь, а старый остается неизменным: - - ```elixir - new_map = Map.put(other_map, :c, 42) - IO.puts(inspect(new_map)) # => %{a: 1, b: 2, c: 42} - IO.puts(inspect(other_map)) # => %{a: 1, b: 2} - ``` - - Для изменения значения существующего ключа есть синтаксический сахар: - - ```elixir - %{other_map | :a => 42} # %{a: 42, b: 2} - %{other_map | :a => 42 , :b => 43} # %{a: 42, b: 43} - ``` - - Такой синтаксис удобен тем, что позволяет изменять несколько ключей сразу. - - А добавить новый ключ этот синтаксис не позволяет: - - ```elixir - %{other_map | :c => 42} # ** (KeyError) key :c not found in: %{a: 1, b: 2} - ``` - - Для удаления ключа используется функция `Map.delete`. Она тоже возвращает новый словарь, а старый остается неизменным: - - ```elixir - Map.delete(other_map, :a) # %{b: 2} - ``` - -instructions: | - - Реализуйте функцию `keys_sum`, которая принимает словарь и два ключа, извлекает значения по этим ключам, и возвращает сумму значений. Если ключа в словаре нет, то соответствующее значение не учитывается. - - Реализуйте функцию `keys_product`, которая принимает словарь и два ключа, извлекает значения по этим ключам, и возвращает произведение значений. Если ключа в словаре нет, то соответствующее значение не учитывается. - - Реализуйте функцию `copy_key`, которая принимает два словаря, ключ, и значение по умолчанию. По ключу нужно извлечь значение из первого словаря и вставить во второй словарь. Если в первом словаре нет такого ключа, то во второй словарь нужно вставить значение по умолчанию. Если во втором словаре уже есть такой ключ, то его значение меняется. - - ```elixir - map = %{a: 1, b: 2, c: 42} - Solution.keys_sum(map, :a, :b) - # => 3 - Solution.keys_sum(map, :a, :c) - # => 43 - Solution.keys_sum(map, :c, :b) - # => 44 - - map = %{one: 1, five: 5, ten: 10} - Solution.keys_product(map, :one, :five) - # => 5 - Solution.keys_product(map, :five, :ten) - # => 50 - Solution.keys_product(map, :two, :ten) - # => 10 - - map1 = %{a: 1, b: 2} - map2 = %{c: 3, d: 4} - - Solution.copy_key(map1, map2, :a, 42) - # => %{c: 3, d: 4, a: 1} - Solution.copy_key(map1, map2, :b, 42) - # => %{c: 3, d: 4, b: 2} - - Solution.copy_key(map2, map1, :d, 42) - # => %{a: 1, b: 2, d: 4} - Solution.copy_key(map2, map1, :e, 42) - # => %{a: 1, b: 2, e: 42} - ``` - - -tips: - - | - [Официальная документация](https://hexdocs.pm/elixir/Map.html) diff --git a/modules/20-data-types/30-maps/en/data.yml b/modules/20-data-types/30-maps/en/data.yml index edc67f5..16e7b3e 100644 --- a/modules/20-data-types/30-maps/en/data.yml +++ b/modules/20-data-types/30-maps/en/data.yml @@ -1,2 +1,3 @@ +--- name: Maps tips: [] diff --git a/modules/20-data-types/30-maps/ru/data.yml b/modules/20-data-types/30-maps/ru/data.yml index b94e00b..21ee578 100644 --- a/modules/20-data-types/30-maps/ru/data.yml +++ b/modules/20-data-types/30-maps/ru/data.yml @@ -1,3 +1,4 @@ +--- name: Словари tips: - | diff --git a/modules/30-flow/10-pattern-matching/description.en.yml b/modules/30-flow/10-pattern-matching/description.en.yml deleted file mode 100644 index 220f662..0000000 --- a/modules/30-flow/10-pattern-matching/description.en.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- -name: Pattern matching -theory: | - - 🚧 In development - -instructions: | - - 🚧 In development - -tips: [] diff --git a/modules/30-flow/10-pattern-matching/description.ru.yml b/modules/30-flow/10-pattern-matching/description.ru.yml deleted file mode 100644 index d0c6c02..0000000 --- a/modules/30-flow/10-pattern-matching/description.ru.yml +++ /dev/null @@ -1,135 +0,0 @@ ---- - -name: Сопоставление с образцом (Pattern Matching) -theory: | - - Одна из главных особенностей функционального программирования -- сопоставление с образцом. Применяется очень широко, так что вряд ли можно найти такую программу на функциональном языке, где нет Pattern Matching (PM). - - Сопоставление с образцом используется для: - - присвоения значений переменным; - - извлечения значений из сложных структур данных; - - условных переходов. - - Рассмотрим все эти случаи на примерах. - - Присвоение значений переменным: - - ```elixir - a = 123 - IO.puts(a) # => 123 - ``` - - Эта элементарная конструкция, которая выглядит как присваивание значения переменной, на самом деле не является присваиванием. Присваивания в Эликсире нет, а оператор `=` называется оператор сопоставления (match operator). - - В данном коде значение справа -- `123`, сопоставляется с шаблоном слева -- переменной `a`. И поскольку шаблон соответствует значению, то сопоставление происходит успешно, и переменная `а` связывается со значением. - - Однако, это тривиальный случай. Чтобы понять PM нужно, рассмотреть более сложные случаи. - - Извлечение значений из сложных структур данных - - ```elixir - user = {:user, "Bob", 25} - {:user, name, age} = user - IO.puts(name) # => Bob - IO.puts(age) # => 25 - ``` - - Первая строка опять выглядит как присваивание. Только значение более сложное -- кортеж из трех элементов. А вот вторая строка уже интереснее. - - Слева от оператора `=` шаблон, который ограничивает множество значений. Этот шаблон может совпасть только с такими значениями, которые являются кортежами из трех элементов, первым элементом обязательно должен быть атом `:user`, а второй и третий элемент могут быть любыми. - - Справа от оператора `=` находится значение, которое мы сравниваем с шаблоном. В данном случае значение извлекается из переменной `user`, но оно может быть и результатом вызова функции или литералом. - - Сопоставление проходит успешно, и в результате переменные шаблона `name` и `age` получают значения `"Bob"` и `25`. - - В случае, если значение не совпадает с шаблоном, возникает исключение: - - ```elixir - {:user, name, age} = {:dog, "Sharik", 5} # ** (MatchError) no match of right hand side value: {:dog, "Sharik", 5} - {:user, name, age} = {:user, "Bob", 25, :developer} # ** (MatchError) no match of right hand side value: {:user, "Bob", 25, :developer} - ``` - - Первое значение не совпало, потому что `:dog != :user`. Второе значение не совпало, потому что в кортеже 4 элемента, а не 3. - - И значение, и шаблон могут быть сложными структурами с любой глубиной вложенности: - - ```elixir - users = [ - {:user, "Bob", :developer, {:lang, ["Erlang", "Elixir"]}}, - {:user, "Bill", :developer, {:lang, ["Python", "JavaScript"]}} - ] - - [{:user, _, _, _}, {:user, name, _, {:lang, [lang1, lang2]}}] = users - IO.puts(name) # => Bill - IO.puts(lang1) # => Python - IO.puts(lang2) # => JavaScript - ``` - - Здесь у нас список из двух элементов. Каждый элемент является кортежем из 4-х элементов. 4-й элемент кортежа, это вложенный кортеж. И в нем еще один вложенный список. Наш шаблон повторяет всю эту структуру и извлекает значения из 4-го уровня вложенности. - - Обратите внимания на символ подчеркивания. Он совпадает с любым значением, и применяется, когда это значение не нужно, мы не хотим сохранять его в переменную. - - Если переменная встречается два раза, то значения в этих местах должны быть одинаковыми: - - ```elixir - {a, a, 42} = {10, 10, 42} # match - {a, a, 42} = {20, 20, 42} # match - {a, a, 42} = {10, 20, 42} # ** (MatchError) no match of right hand side value: {10, 20, 42} - ``` - - Но это не касается символа подчеркивания: - - ```elixir - {_, _, 42} = {10, 10, 42} # match - {_, _, 42} = {20, 20, 42} # match - {_, _, 42} = {10, 20, 42} # match - ``` - - Теперь формализуем то, что мы узнали. Итак, у нас есть оператор сопоставления `=`, слева от него шаблон, и справа значение. - - ```elixir - [pattern] = [value] - ``` - - Шаблон может включать: - - литералы - - переменные - - универсальный шаблон (символ подчеркивания) - - Значение может включать: - - литералы - - переменные - - выражения - - Литералы в шаблоне слева должны совпасть с литералами, переменными, и результатами вычисления значений справа. Все в целом должно совпасть по структуре. Тогда переменные в шаблоне слева получают свои значения из соответствующих позиций справа. Универсальный шаблон совпадает с чем угодно. - - Сопоставление с образцом также используется для ветвлений в коде (условных переходов): - - конструкция case - - клозы функций (clause) - - обработка исключений (rescue, catch) - - чтение сообщений из mailbox процесса (receive) - - Конструкции case и function clause рассмотрим в следующей теме. Обработка исключений и чтение сообщений будут позже в курсе. - -instructions: | - - Реализуйте функцию `get_age(user)`, которая принимает объект `user`, представленный в виде кортежа `{:user, name, age}`, и возвращает возраст (age). - - Реализуйте функцию `get_names(users)`, которая принимает список из трёх объектов `user`, представленных такими же кортежами, и возвращает список из трёх имен. - - ```elixir - bob = {:user, "Bob", 42} - helen = {:user, "Helen", 20} - kate = {:user, "Kate", 22} - - Solution.get_age(bob) # => 42 - Solution.get_age(helen) # => 20 - Solution.get_age(kate) # => 22 - - Solution.get_names([bob, helen, kate]) - # => ["Bob", "Helen", "Kate"] - ``` - -tips: - - | - [Про pattern matching](https://ru.wikipedia.org/wiki/%D0%A1%D0%BE%D0%BF%D0%BE%D1%81%D1%82%D0%B0%D0%B2%D0%BB%D0%B5%D0%BD%D0%B8%D0%B5_%D1%81_%D0%BE%D0%B1%D1%80%D0%B0%D0%B7%D1%86%D0%BE%D0%BC) diff --git a/modules/30-flow/10-pattern-matching/en/data.yml b/modules/30-flow/10-pattern-matching/en/data.yml index 1841cd1..c296d3f 100644 --- a/modules/30-flow/10-pattern-matching/en/data.yml +++ b/modules/30-flow/10-pattern-matching/en/data.yml @@ -1,2 +1,3 @@ +--- name: Pattern matching tips: [] diff --git a/modules/30-flow/10-pattern-matching/ru/data.yml b/modules/30-flow/10-pattern-matching/ru/data.yml index c9fe089..062a938 100644 --- a/modules/30-flow/10-pattern-matching/ru/data.yml +++ b/modules/30-flow/10-pattern-matching/ru/data.yml @@ -1,3 +1,4 @@ +--- name: Сопоставление с образцом (Pattern Matching) tips: - > diff --git a/modules/30-flow/15-pattern-matching-for-maps/description.en.yml b/modules/30-flow/15-pattern-matching-for-maps/description.en.yml deleted file mode 100644 index 4fc9e1a..0000000 --- a/modules/30-flow/15-pattern-matching-for-maps/description.en.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- -name: Pattern matching for maps -theory: | - - 🚧 In development - -instructions: | - - 🚧 In development - -tips: [] diff --git a/modules/30-flow/15-pattern-matching-for-maps/description.ru.yml b/modules/30-flow/15-pattern-matching-for-maps/description.ru.yml deleted file mode 100644 index c5ed81b..0000000 --- a/modules/30-flow/15-pattern-matching-for-maps/description.ru.yml +++ /dev/null @@ -1,90 +0,0 @@ ---- - -name: Сопоставление с образцом для словарей -theory: | - - Есть некоторые нюансы сопоставления с образцом при работе со словарями. В шаблоне не нужно перечислять все ключи, какие есть в словаре. Мы указываем только те ключи, которые нам нужны: - - ```elixir - my_map = %{a: 1, b: 2, c: 3} - %{a: value} = my_map - - IO.puts(value) # => 1 - ``` - - Если ключи не являются атомами, то синтаксис отличается: - - ```elixir - my_map = %{"a" => 1, "b" => 2, "c" => 3} - %{"a" => value1} = my_map - IO.puts(value1) # => 1 - - %{"b" => value2, "c" => value3} = my_map - IO.puts(value2) # => 2 - IO.puts(value3) # => 3 - ``` - - Шаблон `%{}` совпадает с любым словарём. Это контринтуитивно, можно было бы ожидать, что этот шаблон совпадает только с пустым словарём. Этим шаблоном нельзя ничего извлечь, но можно проверить, что значение является словарём, а не чем-то иным. - - ```elixir - my_map = %{"a" => 1, "b" => 2, "c" => 3} - %{} = my_map - my_map = 42 - %{} = my_map # ** (MatchError) no match of right hand side value: 42 - ``` - - Переменные можно использовать для извлечения значений, но не для извлечения ключей: - - ```elixir - %{"c" => my_var} = my_map - IO.puts(my_var) # => 3 - - %{my_var => 1} = my_map # ** (CompileError) iex:17: cannot use variable my_var as map key inside a pattern. - ``` - - Переменная в шаблоне может выполнять две роли. Либо мы хотим, чтобы эта переменная получила новое значение, и тогда не важно, использовалась ли эта переменная раньше, было ли у нее какое-то значение. Либо мы хотим использовать значение, которое переменная уже имеет, как часть шаблона. Во втором случае понадобится _pin_ operator. - - ```elixir - animal = :cat - {^animal, "Tihon"} = {:cat, "Tihon"} - {^animal, "Tihon"} = {:dog, "Tihon"} # ** (MatchError) no match of right hand side value: {:dog, "Tihon"} - ``` - - _pin_ operator извлекает текущее значение переменной и подставляет его в шаблон. И дальше это значение в шаблоне работает как литерал. - - _pin_ operator можно использовать и для ключа, и для значения: - - ```elixir - value1 = 1 - %{"a" => ^value1} = my_map - keyb = "b" - %{^keyb => _} = my_map - ``` - -instructions: | - - Реализовать функцию `get_values(data)`, которая принимает словарь, извлекает из него два значения по ключам `:a` и `:b`, и возвращает их в виде кортежа `{a_value, b_value}`. - - Реализовать функцию `get_value_by_key(data, key)`, которая принимает словарь и ключ, извлекает значение по указанному ключу и возвращает его. - - Обе функции генерируют исключение `MatchError` если в словаре нет нужных ключей. - - ```elixir - Solution.get_values(%{a: 1, b: 2}) - # => {1, 2} - Solution.get_values(%{a: :ok, b: 42, c: true}) - # => {:ok, 42} - - Solution.get_values(%{}) - # => MatchError - - Solution.get_value_by_key(%{answer: 42}, :answer) - # => 42 - Solution.get_value_by_key(%{question: "6 * 7"}, :question) - # => "6 * 7" - - Solution.get_value_by_key(%{a: 1}, :b) - # => MatchError - ``` - -tips: [] diff --git a/modules/30-flow/15-pattern-matching-for-maps/en/data.yml b/modules/30-flow/15-pattern-matching-for-maps/en/data.yml index 127ab7e..80310ad 100644 --- a/modules/30-flow/15-pattern-matching-for-maps/en/data.yml +++ b/modules/30-flow/15-pattern-matching-for-maps/en/data.yml @@ -1,2 +1,3 @@ +--- name: Pattern matching for maps tips: [] diff --git a/modules/30-flow/15-pattern-matching-for-maps/ru/data.yml b/modules/30-flow/15-pattern-matching-for-maps/ru/data.yml index 67f9c90..5436c70 100644 --- a/modules/30-flow/15-pattern-matching-for-maps/ru/data.yml +++ b/modules/30-flow/15-pattern-matching-for-maps/ru/data.yml @@ -1,2 +1,3 @@ +--- name: Сопоставление с образцом для словарей tips: [] diff --git a/modules/30-flow/20-case/description.en.yml b/modules/30-flow/20-case/description.en.yml deleted file mode 100644 index 56c33fa..0000000 --- a/modules/30-flow/20-case/description.en.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- -name: Case -theory: | - - 🚧 In development - -instructions: | - - 🚧 In development - -tips: [] diff --git a/modules/30-flow/20-case/description.ru.yml b/modules/30-flow/20-case/description.ru.yml deleted file mode 100644 index faf1129..0000000 --- a/modules/30-flow/20-case/description.ru.yml +++ /dev/null @@ -1,148 +0,0 @@ ---- - -name: Конструкция case -theory: | - - Условные переходы в функциональных языках отличаются от императивных, потому что основаны на сопоставлении с образцом. Основная идея в том, что некое значение по очереди сравнивается с несколькими шаблонами, и в зависимости от того, с каким шаблоном оно совпадет, выполняется та или иная ветка кода. - - Есть несколько вариантов условных переходов: - - конструкция `case`; - - конструкция `cond`; - - тело функции (function clause); - - обработка исключений `rescue`, `catch`; - - чтение сообщений из mailbox `receive`. - - Все они, кроме *cond*, реализуют эту идею. - - ## Рассмотрим конструкцию case. - - Для примера рассмотрим вычисление наибольшего общего делителя: - - ```elixir - def gcd(a, b) do - case rem(a, b) do - 0 -> b - c -> gcd(b, c) - end - end - ``` - - Здесь вычисляется значение `rem(a, b)` и сравнивается с двумя шаблонами. Первый шаблон -- литерал `0`. Если значение совпадает с ним, то выполняется код, завершающий рекурсию и возвращающий `b`. Второй шаблон -- переменная `c`. С этим шаблоном совпадут любые значения, и тогда выполняется вызов `gcd(b, c)`. - - Второй пример: - - ```elixir - case Map.fetch(acc, word) do - {:ok, count} -> Map.put(acc, word, count + 1) - :error -> Map.put(acc, word, 1) - end - ``` - - Здесь выполняется вызов функции `Map.fetch(acc, word)`. Получившееся значение сравнивается с двумя шаблонами и выполняется соответствующий код. - - Шаблонов может быть несколько. И важен их порядок, потому что первый совпавший шаблон останавливает перебор оставшихся шаблонов. Если не совпал ни один из шаблонов, то генерируется исключение. - - В общем виде конструкция *case* выглядит так: - - ```elixir - case Expr do - Pattern1 [when GuardSequence1] -> - Body1 - ... - PatternN [when GuardSequenceN] -> - BodyN - end - ``` - - Что такое GuardSequence -- цепочка охранных выражений, мы рассмотрим позже. - - case могут быть вложенными друг в друга: - - ```elixir - def handle(animal, action) do - case animal do - {:dog, name} -> - case action do - :add -> IO.puts("add dog #{name}") - :remove -> IO.puts("remove dog #{name}") - end - {:cat, name} -> - case action do - :add -> IO.puts("add cat #{name}") - :remove -> IO.puts("remove cat #{name}") - end - end - end - ``` - - Вложенный даже на два уровня код плохо читается. Обычно этого можно избежать. Данный пример можно реализовать без вложенного case таким образом: - - ```elixir - def handle(animal, action) do - case {animal, action} do - {{:dog, name}, :add} -> IO.puts("add dog #{name}") - {{:dog, name}, :remove} -> IO.puts("remove dog #{name}") - {{:cat, name}, :add} -> IO.puts("add cat #{name}") - {{:cat, name}, :remove} -> IO.puts("remove cat #{name}") - end - end - ``` - - ## Охранные выражения (Guards) - - Теперь вернемся к упомянутым выше охранным выражениям. - - Не всегда достаточно шаблона, чтобы проверить все условия для ветвления в коде. Например, шаблоном нельзя проверить попадание числа в определенный диапазон. - - ```elixir - def handle4(animal) do - case animal do - {:dog, name, age} when age > 10 -> IO.puts("#{name} is a dog older than 10") - {:dog, name, _} -> IO.puts("#{name} is a 10 years old or younger dog") - {:cat, name, age} when age > 10 -> IO.puts("#{name} is a cat older than 10") - {:cat, name, _} -> IO.puts("#{name} is a 10 years old or younger cat") - end - end - ``` - - Охранное выражение представляет собой предикат или цепочку предикатов: - - ```elixir - when predicat1 and predicat2 or ... predicatN -> - ``` - - В предикатах можно использовать ограниченный набор функций, описанный в [документации](https://hexdocs.pm/elixir/patterns-and-guards.html#list-of-allowed-functions-and-operators). Некоторые функциональные языки разрешают вызывать любые функции в охранных выражениях. Но Эликсир не относится к таким языкам. - - Если при вычислении охранного выражения возникает исключение, то оно не приводит к остановке процесса, а приводит к тому, что все выражение вычисляется в false. Это позволяет писать выражения проще. Вместо: - - ```elixir - when is_map(a) and map_size(a) > 10 -> - ``` - - можно сразу писать: - - ```elixir - when map_size(a) > 10 -> - ``` - -instructions: | - - Реализовать функцию `join_game(user)`, которая принимает игрока в виде кортежа `{:user, name, age, role}` и определяет, разрешено ли данному игроку подключиться к игре. Если игроку уже исполнилось 18 лет, то он может войти в игру. Если роль игрока `:admin` или `:moderator`, то он может войти в игру независимо от возраста. Функция должна вернуть `:ok` или `:error`. - - Реализовать функцию `move_allowed?(current_color, figure)` которая определяет, разрешено ли данной шахматной фигуре сделать ход. Параметр `current_color` может быть либо `:white` либо `:black`, и он указывает, фигурам какого цвета разрешено сделать ход. Параметр `figure` представлен кортежем `{type, color}`, где `type` может быть один из: `:pawn`, `:rock`, `:bishop`, `:knight`, `:queen`, `:king`, а color может быть `:white` или `:black`. Фигура может сделать ход если её тип `:pawn` или `:rock` и её цвет совпадает с `current_color`. Функция должна вернуть `true` или `false`. - - ```elixir - Solution.join_game({:user, "Bob", 17, :admin}) - # => :ok - Solution.join_game({:user, "Bob", 17, :moderator}) - # => :ok - Solution.join_game({:user, "Bob", 17, :member}) - # => :error - - Solution.move_allowed?(:white, {:pawn, :white}) - # => true - Solution.move_allowed?(:black, {:pawn, :white}) - # => false - ``` - -tips: [] diff --git a/modules/30-flow/20-case/en/data.yml b/modules/30-flow/20-case/en/data.yml index 347395e..ec2da1d 100644 --- a/modules/30-flow/20-case/en/data.yml +++ b/modules/30-flow/20-case/en/data.yml @@ -1,2 +1,3 @@ +--- name: Case tips: [] diff --git a/modules/30-flow/20-case/ru/data.yml b/modules/30-flow/20-case/ru/data.yml index 75b2bcf..5b07315 100644 --- a/modules/30-flow/20-case/ru/data.yml +++ b/modules/30-flow/20-case/ru/data.yml @@ -1,2 +1,3 @@ +--- name: Конструкция case tips: [] diff --git a/modules/30-flow/30-cond/description.en.yml b/modules/30-flow/30-cond/description.en.yml deleted file mode 100644 index cc9d67a..0000000 --- a/modules/30-flow/30-cond/description.en.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- -name: Cond -theory: | - - 🚧 In development - -instructions: | - - 🚧 In development - -tips: [] diff --git a/modules/30-flow/30-cond/description.ru.yml b/modules/30-flow/30-cond/description.ru.yml deleted file mode 100644 index 28efbac..0000000 --- a/modules/30-flow/30-cond/description.ru.yml +++ /dev/null @@ -1,136 +0,0 @@ ---- - -name: Конструкция cond -theory: | - - Если из конструкции *case* убрать шаблоны, но оставить охранные выражения, то получится конструкция *cond*. - - Было: - - ```elixir - case Expr do - Pattern1 [when GuardSequence1] -> - Body1 - ... - PatternN [when GuardSequenceN] -> - BodyN - end - ``` - - Стало: - - ```elixir - cond do - GuardSequence1 -> - Body1 - ... - GuardSequenceN -> - BodyN - end - ``` - - В принципе, это эквивалент цепочки `if...else if`, которая нередко встречается в императивных языках. Python, например: - - ```python - a = int(input()) - if a < -5: - print('Low') - elif -5 <= a <= 5: - print('Mid') - else: - print('High') - ``` - - Как и в конструкции *case*, очередность выражений важна. И если ни одно из выражений не вычислилось в true, то возникает исключение. - - ```elixir - def my_fun(num) do - cond do - num > 10 -> IO.puts("more than 10") - num > 5 -> IO.puts("more than 5") - end - end - - my_fun(20) # => more than 10 - my_fun(8) # => more than 5 - my_fun(3) # ** (CondClauseError) no cond clause evaluated to a truthy value - ``` - - ## Конструкция if - - В Эликсир есть привычная всем конструкция *if*: - - ```elixir - def gcd(a, b) do - if rem(a, b) == 0 do - b - else - gcd(b, c) - end - end - ``` - - Только это не настоящая конструкция языка, а макрос. Впрочем, в Эликсир очень многое является макросами. В некоторых случаях это важно знать, в некоторых не важно. - - Есть и конструкция *unless*, тоже макрос: - - ```elixir - def gcd(a, b) do - unless rem(a, b) != 0 do - b - else - gcd(b, c) - end - end - ``` - - Есть важное отличие от императивных языков -- в функциональных языках *if* всегда возвращает какое-то значение. - - ```elixir - a = 5 - b = 10 - c = if a > b do - a - else - b - end - - IO.puts(c) - # => 10 - ``` - - Некоторые функциональные языки требуют, чтобы часть *else* всегда присутствовала, потому что значение нужно вернуть в любом случае, выполняется условие *if* или не выполняется. Эликсир этого не требует: - - ```elixir - c = if a > b do - a - end - - IO.puts(c) # => nil - ``` - -instructions: | - - Реализовать функцию `single_win?(a_win, b_win)`, которая принимает 2 булевых параметра: `a_win` -- победил ли игрок A, и `b_win` -- победил ли игрок B. Функция возвращает `true` если победил только один из двоих игроков, иначе возвращает `false`. - - Реализовать функцию `double_win?(a_win, b_win, c_win)`, которая принимает 3 булевых параметра для трех игроков. Если победили игроки A и B, то функция возвращает атом `:ab`. Если победили игроки A и C, то функция возвращает атом `:ac`, если победили игроки B и C, то функция возвращает атом `:bc`. Во всех остальных случаях функция возвращает `false`. - - ```elixir - Solution.single_win?(true, false) - # => true - Solution.single_win?(false, true) - # => true - Solution.single_win?(true, true) - # => false - - Solution.double_win?(true, true, false) - # => :ab - Solution.double_win?(true, false, true) - # => :ac - Solution.double_win?(true, true, true) - # => false - Solution.double_win?(true, false, false) - # => false - ``` - -tips: [] diff --git a/modules/30-flow/30-cond/en/data.yml b/modules/30-flow/30-cond/en/data.yml index bbc1bee..489829c 100644 --- a/modules/30-flow/30-cond/en/data.yml +++ b/modules/30-flow/30-cond/en/data.yml @@ -1,2 +1,3 @@ +--- name: Cond tips: [] diff --git a/modules/30-flow/30-cond/ru/data.yml b/modules/30-flow/30-cond/ru/data.yml index 99abff8..40ab20a 100644 --- a/modules/30-flow/30-cond/ru/data.yml +++ b/modules/30-flow/30-cond/ru/data.yml @@ -1,2 +1,3 @@ +--- name: Конструкция cond tips: [] diff --git a/modules/30-flow/40-function-clause/description.en.yml b/modules/30-flow/40-function-clause/description.en.yml deleted file mode 100644 index 510b86c..0000000 --- a/modules/30-flow/40-function-clause/description.en.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- -name: Closure -theory: | - - 🚧 In development - -instructions: | - - 🚧 In development - -tips: [] diff --git a/modules/30-flow/40-function-clause/description.ru.yml b/modules/30-flow/40-function-clause/description.ru.yml deleted file mode 100644 index 6ce4051..0000000 --- a/modules/30-flow/40-function-clause/description.ru.yml +++ /dev/null @@ -1,112 +0,0 @@ ---- - -name: Тело функции (function clause) -theory: | - - В Эликсир одна функция может иметь несколько тел -- несколько разных блоков кода. В зависимости от входящих аргументов выполняется только один из этих блоков. - - По английски термин "тело функции" пишется *clause* и произносится `[klôz]`. Поскольку это короче, то все Эликсир разработчики предпочитают говорить "клоз" вместо "тело функции". - - ```elixir - def handle({:dog, name}, :add) do - IO.puts("add dog #{name}") - end - - def handle({:dog, name}, :remove) do - IO.puts("remove dog #{name}") - end - - def handle({:cat, name}, :add) do - IO.puts("add cat #{name}") - end - - def handle({:cat, name}, :remove) do - IO.puts("remove cat #{name}") - end - ``` - - Здесь функция `handle/2` имеет 4 тела. Шаблоны описываются прямо в аргументах функции, отдельно для каждого тела. Принцип такой же, как и с конструкцией *case* -- шаблоны проверяются по очереди на совпадение с входящими аргументами функции. Первый совпавший шаблон вызывает соответствующий блок кода и останавливает дальнейший перебор. Если ни один шаблон не совпал, то генерируется исключение. - - Как и в случае с *case*, здесь тоже важна очередность шаблонов. Типичная ошибка -- расположить более общий шаблон выше, чем более специфичный шаблон: - - ```elixir - def handle(animal, action) do - IO.puts("do something") - end - - def handle({:dog, name}, :add) do - IO.puts("add dog #{name}") - end - ``` - - Во многих таких случаях компилятор выдаст предупреждение: - - ```elixir - warning: this clause for handle/2 cannot match because a previous clause at line 27 always matches - ``` - - Но бывает, что компилятор не замечает проблему. - - Как и с *case*, с телами функций могут использоваться охранные выражения: - - ```elixir - def handle({:dog, name, age}) when age > 10 do - IO.puts("#{name} is a dog older than 10") - end - - def handle({:dog, name, _age}) do - IO.puts("#{name} is a 10 years old or younger dog") - end - - def handle({:cat, name, age}) when age > 10 do - IO.puts("#{name} is a cat older than 10") - end - - def handle({:cat, name, _age}) do - IO.puts("#{name} is a 10 years old or younger cat") - end - ``` - - Конструкция *case* и тела функций полностью эквивалентны друг другу. Выбор того или иного варианта является личным предпочтением разработчика. - -instructions: | - - Поиграем в "крестики-нолики". Игровая доска размером 3х3 ячеек представлена кортежем из 3-х кортежей: - - ```elixir - { - {:x, :o, :f}, - {:f, :o, :f}, - {:f, :f, :f} - } - ``` - - Каждая ячейка может находиться в одном из 3-х состояний: - - `:x` в ячейке стоит крестик; - - `:o` в ячейке стоит нолик; - - `:f` ячейка свободна. - - Реализовать функцию `valid_game?(state)`, которая получает на вход состояние игры, и проверяет, является ли это состояние валидным. То есть, имеет ли состояние правильную структуру, и заполнены ли ячейки только валидными значениями. Функция возвращает булевое значение. - - Реализовать функцию `check_who_win(state)`, которая получает состояние и возвращает победителя, если он есть. Функция должна определить наличие трех крестиков или трех ноликов по горизонтали, или по вертикали, или по диагонали. В случае победы крестиков функция возвращает `{:win, :x}`, в случае победы ноликов функция возвращает `{:win, :o}`, если победы нет, функция возвращает `:no_win`. - - ```elixir - Solution.valid_game?({{:x, :x, :x}, {:x, :x, :x}, {:x, :x, :x}}) - # => true - Solution.valid_game?({{:f, :f, :f}, {:f, :f, :f}, {:f, :f, :f}}) - # => true - - Solution.valid_game?({{:x, :o, :some}, {:f, :x, :o}, {:o, :o, :x}}) - # => false - Solution.valid_game?({{:x, :o, :f}, {:f, :x, :x, :o}, {:o, :o, :x}}) - # => false - - Solution.check_who_win({{:x, :x, :x}, {:f, :f, :o}, {:f, :f, :o}}) - # => {:win, :x} - Solution.check_who_win({{:o, :x, :f}, {:o, :f, :x}, {:o, :f, :f}}) - # => {:win, :o} - Solution.check_who_win({{:x, :f, :f}, {:f, :x, :x}, {:f, :f, :o}}) - # => :no_win - ``` - -tips: [] diff --git a/modules/30-flow/40-function-clause/en/data.yml b/modules/30-flow/40-function-clause/en/data.yml index 48bea05..206d8c4 100644 --- a/modules/30-flow/40-function-clause/en/data.yml +++ b/modules/30-flow/40-function-clause/en/data.yml @@ -1,2 +1,3 @@ +--- name: Closure tips: [] diff --git a/modules/30-flow/40-function-clause/ru/data.yml b/modules/30-flow/40-function-clause/ru/data.yml index 20a7556..688e943 100644 --- a/modules/30-flow/40-function-clause/ru/data.yml +++ b/modules/30-flow/40-function-clause/ru/data.yml @@ -1,2 +1,3 @@ +--- name: Тело функции (function clause) tips: [] diff --git a/modules/30-flow/50-pipeline/description.en.yml b/modules/30-flow/50-pipeline/description.en.yml deleted file mode 100644 index 186aac1..0000000 --- a/modules/30-flow/50-pipeline/description.en.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- -name: Pipeline -theory: | - - 🚧 In development - -instructions: | - - 🚧 In development - -tips: [] diff --git a/modules/30-flow/50-pipeline/description.ru.yml b/modules/30-flow/50-pipeline/description.ru.yml deleted file mode 100644 index 5ea9859..0000000 --- a/modules/30-flow/50-pipeline/description.ru.yml +++ /dev/null @@ -1,56 +0,0 @@ ---- - -name: Оператор pipe -theory: | - - Одна из фишек Эликсир -- оператор *pipe*. Это удобный способ написания цепочек вызовов функций, где результат одной функции является аргументом для другой. - - В большинстве языков в такой ситуации используют либо временные переменные: - - ```elixir - a = func1() - b = func2(a) - c = func3(b) - ``` - - Либо вложенные вызовы: - - ```elixir - func3(func2(func1()) - ``` - - Но pipe позволяет написать код короче и с естественным порядком функций в цепочке: - - ```elixir - func1() |> func2() |> func3() - ``` - - Оператор принимает значение из одной функции, и передаёт его первым аргументом во вторую функцию. Если у функции больше одного аргумента, то их нужно указать явно: - - ```elixir - Map.put(%{}, :a, 1) |> Map.put(:b, 2) |> Map.put(:c, 3) # %{a: 1, b: 2, c: 3} - ``` - - `pipe` настолько важен, что все функции системных библиотек рассчитаны на его использование. Например, большинство функций в модуле *Map* первым аргументом принимают Map, поэтому их легко собирать в цепочку. - - ```elixir - Map.new() |> Map.put_new(:a, 1) |> Map.to_list() # [a: 1] - ``` - - Рекомендуется придерживаться такого же подхода в своих собственных функциях. - -instructions: | - - Изучите документацию для модуля *String*. Найдите функции для удаления пробельных символов в начале и конце строки, для приведения строки к нижнему регистру, и для дублирования строки несколько раз. - - Нужно собрать эти три функции в цепочку, таким образом, чтобы строка на входе была очищена от лишних пробелов, приведена в нижний регистр и продублирована указанное количество раз: - - ```elixir - Solution.process(" My Cool String ", 2) # "my cool stringmy cool string" - ``` - -tips: - - | - [Документация для String](https://hexdocs.pm/elixir/String.html) - - | - [Официальная документация](https://hexdocs.pm/elixir/Kernel.html#%7C%3E/2) diff --git a/modules/30-flow/50-pipeline/en/data.yml b/modules/30-flow/50-pipeline/en/data.yml index 5eb599c..6d31a19 100644 --- a/modules/30-flow/50-pipeline/en/data.yml +++ b/modules/30-flow/50-pipeline/en/data.yml @@ -1,2 +1,3 @@ +--- name: Pipeline tips: [] diff --git a/modules/30-flow/50-pipeline/ru/data.yml b/modules/30-flow/50-pipeline/ru/data.yml index c89353e..3d3b75d 100644 --- a/modules/30-flow/50-pipeline/ru/data.yml +++ b/modules/30-flow/50-pipeline/ru/data.yml @@ -1,3 +1,4 @@ +--- name: Оператор pipe tips: - | diff --git a/modules/35-fp/10-recursion/description.en.yml b/modules/35-fp/10-recursion/description.en.yml deleted file mode 100644 index 49fd6f4..0000000 --- a/modules/35-fp/10-recursion/description.en.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- -name: Recursion -theory: | - - 🚧 In development - -instructions: | - - 🚧 In development - -tips: [] diff --git a/modules/35-fp/10-recursion/description.ru.yml b/modules/35-fp/10-recursion/description.ru.yml deleted file mode 100644 index b6edd29..0000000 --- a/modules/35-fp/10-recursion/description.ru.yml +++ /dev/null @@ -1,109 +0,0 @@ ---- - -name: Рекурсия -theory: | - - В Эликсир, как и во всех функциональных языках, нет циклов. Их заменяет рекурсия. - - ## Пример 1 - - Начнем сразу с практики, рассмотрим пример функции, которая вычисляет длину списка: - - ```elixir - def len([]), do: 0 - def len([_head | tail]) do - 1 + len(tail) - end - - len([1,2,3,4,5]) # 5 - len([]) # 0 - ``` - - Функция `len` состоит из двух тел. Первое тело вызывается на пустом списке, и это наше условие выхода из рекурсии. Здесь все просто -- длина пустого списка 0. Второе тело вызывается на непустом списке, и это один шаг (одна итерация) обработки списка. - - Здесь мы делаем классическое для функционального программирования разделения списка на голову и хвост: - - ```elixir - [_head | tail] - ``` - - (Переменная `_head` в дальнейшем не используется, поэтому её имя начинается с подчеркивания). Здесь логика тоже очень простая -- длина списка, это 1 плюс длина хвоста списка. - - На каждой итерации список уменьшается на один элемент, пока не становится пустым. А пустой список попадает в первое тело функции, что завершает рекурсию. - - Для списка `[1, 2, 3, 4, 5]` стек вызовов функции будет выглядеть так: - - ```elixir - len([1,2,3,4,5]) - 1 + len([2,3,4,5]) - 1 + 1 + len([3,4,5]) - 1 + 1 + 1 + len([4,5]) - 1 + 1 + 1 + 1 + len([5]) - 1 + 1 + 1 + 1 + 1 + len([]) - 1 + 1 + 1 + 1 + 1 + 0 - ``` - - ## Пример 2 - - Возьмем пример немного сложнее, реализуем функцию, которая возвращает максимальный элемент в списке. - - ```elixir - def list_max([]), do: nil - def list_max([elem]), do: elem - def list_max([head | tail]) do - max(head, list_max(tail)) - end - - list_max([1,3,33,42,100500,-10]) # 100500 - list_max([1]) # 1 - list_max([]) # nil - ``` - - Нюанс в том, что в пустом списке нет максимального элемента, так что нужно вернуть `nil`. Условием завершения рекурсии будет список из одного элемента. Очевидно, что этот элемент и есть максимальный элемент. - - И третье тело функции реализует шаг итерации на списке, содержащем больше одного элемента. Опять делим список на голову и хвост. Чтобы найти максимальный элемент, нужно сравнить голову с максимальным элементом хвоста. - - Для списка `[1, 3, 33, 42]` стек вызовов будет выглядеть так: - - ```elixir - list_max([1,3,33,42]) - max(1, list_max([3,33,42])) - max(1, max(3, list_max([33,42]))) - max(1, max(3, max(33, list_max([42])))) - max(1, max(3, max(33, 42))) - max(1, max(3, 42)) - max(1, 42) - 42 - ``` - - ## Пример 3 - - Реализуем функцию, которая меняет местами пары чисел в списке. - - ```elixir - def swap_pair([]), do: [] - def swap_pair([_]), do: raise "Can't swap a list with an odd number of elements" - def swap_pair([a, b | tail]) do - [b, a | swap_pair(tail)] - end - - swap_pair([1,2,3,4]) # [2, 1, 4, 3] - TR.swap_pair([1,2,3]) # ** (RuntimeError) Can't swap a list with an odd number of elements - ``` - - Здесь две особенности. Во-первых, функция извлекает не одну голову из списка, а сразу две. Оператор *|* позволяет извлекать сразу несколько элементов. Во-вторых, функция предъявляет требование к входящим данным -- список должен иметь четное количество элементов. - - -instructions: | - - Реализовать функцию `range(from, to)`, которая принимает два числа, задающих диапазон, и возвращает список, заполненный числами в этом диапазоне, `from` и `to` включительно: - - ```elixir - Solution.range(1, 5) # [1, 2, 3, 4, 5] - Solution.range(2, 2) # [2] - Solution.range(3, 2) # [] - ``` - -tips: - - | - [Статья в Hexlet про рекурсию](https://ru.hexlet.io/blog/posts/recursive) diff --git a/modules/35-fp/10-recursion/en/data.yml b/modules/35-fp/10-recursion/en/data.yml index 65245e1..20f15dc 100644 --- a/modules/35-fp/10-recursion/en/data.yml +++ b/modules/35-fp/10-recursion/en/data.yml @@ -1,2 +1,3 @@ +--- name: Recursion tips: [] diff --git a/modules/35-fp/10-recursion/ru/data.yml b/modules/35-fp/10-recursion/ru/data.yml index 60a0f01..4f622ef 100644 --- a/modules/35-fp/10-recursion/ru/data.yml +++ b/modules/35-fp/10-recursion/ru/data.yml @@ -1,3 +1,4 @@ +--- name: Рекурсия tips: - | diff --git a/modules/35-fp/20-recursion-with-acc/description.en.yml b/modules/35-fp/20-recursion-with-acc/description.en.yml deleted file mode 100644 index 10787ad..0000000 --- a/modules/35-fp/20-recursion-with-acc/description.en.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- -name: Recursion with accumulator -theory: | - - 🚧 In development - -instructions: | - - 🚧 In development - -tips: [] diff --git a/modules/35-fp/20-recursion-with-acc/description.ru.yml b/modules/35-fp/20-recursion-with-acc/description.ru.yml deleted file mode 100644 index b069401..0000000 --- a/modules/35-fp/20-recursion-with-acc/description.ru.yml +++ /dev/null @@ -1,147 +0,0 @@ ---- - -name: Рекурсивные функции с аккумуляторами -theory: | - - Типичный способ работы со списками -- использование рекурсивных функций и аккумуляторов. Рассмотрим эту тему на примерах. - - ## Пример 1 - - Допустим, у нас есть список пользователей: - - ```elixir - users = [ - {:user, 1, "Bob", 23}, - {:user, 2, "Helen", 20}, - {:user, 3, "Bill", 15}, - {:user, 4, "Kate", 14} - ] - ``` - - Здесь каждый пользователь представлен кортежем из 4-х элементов: - - :user -- тэг для обозначения типа данных; - - id -- идентификатор пользователя; - - name -- имя пользователя; - - age -- и его возраст. - - И мы хотим отфильтровать совершеннолетних пользователей. То есть, получить новый список, где будут только пользователи старше 16 лет. - - Применим рекурсивную функцию с аккумулятором: - - ```elixir - defmodule Solution do - def filter_adults(users), do: filter_adults(users, []) - - defp filter_adults([], acc), do: Enum.reverse(acc) - - defp filter_adults([user | users], acc) do - {:user, _, _, age} = user - if age > 16 do - filter_adults(users, [user | acc]) - else - filter_adults(users, acc) - end - end - end - - Solition.filter_adults(users) - # => [ - # => {:user, 1, "Bob", 23}, - # => {:user, 2, "Helen", 20} - # => ] - ``` - - Функция получает на вход два аргумента: список пользователей, и ещё один список, где будет накапливаться результат выполнения -- аккумулятор. В начале выполнения список пользователей полный, а аккумулятор пустой. На каждом шаге итерации мы будем брать один элемент из первого списка, и, если он нам нужен, то класть его в аккумулятор. В итоге первый список опустошится, а аккумулятор наполнится. - - Наша рекурсивная функция имеет два тела (_clause_). Первое тело выполняется, когда список пользователей пустой. То есть, это завершение фильтрации. И тут нужно просто отдать накопленный результат. - - Второе тело и делает всю работу. Сперва список пользователей с помощью оператора cons делится на голову и хвост (это можно делать прямо в описании аргументов функции). Голова -- это текущий элемент списка, который мы будем анализировать. Хвост -- это остаток списка, который мы передадим дальше, в следующий рекурсивный вызов. - - Анализ элемента тут простой: определяем возраст пользователя. Пользователей младше 16 лет мы игнорируем, а старше добавляем в аккумулятор, опять с помощью оператора cons. - - Элементы в аккумуляторе накапливаются в обратном порядке. Если это не важно, то аккумулятор можно вернуть, как есть. Если желательно сохранить оригинальный порядок, то аккумулятор нужно развернуть. - - - ## Пример 2 - - Второй пример. Допустим, из списка пользователей нужно извлечь их идентификаторы и имена. То есть, получить список кортежей вида: - - ```elixir - {id, name} - ``` - - Реализация: - - ```elixir - defmodule Solution do - def get_id_name(users), do: get_id_name(users, []) - - defp get_id_name([], acc), do: Enum.reverse(acc) - - defp get_id_name([user | users], acc) do - {:user, id, name, _} = user - get_id_name(users, [{id, name} | acc]) - end - end - - Solution.get_id_name(users) - # => [{1, "Bob"}, {2, "Helen"}, {3, "Bill"}, {4, "Kate"}] - ``` - - Опять два тела у функции. Первое тело отвечает за завершение рекурсии и возврат результата, накопленного в аккумуляторе. - - Второе тело обрабатывает каждый элемент списка, извлекает из кортежа нужные `id` и `name`, и сохраняет их в аккумуляторе. - - Хорошая практика -- предлагать как публичный АПИ функцию с одним аргументом. А функцию с аккумулятором скрывать от пользователей модуля. Потому что аккумулятор может быть сложнее, чем просто список, и пользователям модуля незачем об этом знать. - - - ## Пример 3 - - И вот пример со сложным аккумулятором. Допустим нам нужно разделить пользователей на два списка: несовершеннолетние и взрослые. - - ```elixir - defmodule Solution do - def split_teens_and_adults(users), do: split_teens_and_adults(users, {[], []}) - - defp split_teens_and_adults([], {teens, adults}) do - {Enum.reverse(teens), Enum.reverse(adults)} - end - - defp split_teens_and_adults([user | users], {teens, adults}) do - {:user, _, _, age} = user - if age > 16 do - split_teens_and_adults(users, {teens, [user | adults]}) - else - split_teens_and_adults(users, {[user | teens], adults}) - end - end - end - - Solution.split_teens_and_adults(users) - # => {[{:user, 3, "Bill", 15}, {:user, 4, "Kate", 14}], - # => [{:user, 1, "Bob", 23}, {:user, 2, "Helen", 20}]} - ``` - - В этот раз наш аккумулятор -- это кортеж из двух списков. В первом теле функции, как обычно, возвращаем результат. И тут мы разворачиваем оба списка. Во втором теле функции обрабатываем каждый элемент списка, извлекаем и проверяем возраст. Тех, кто моложе 16 лет, кладем в первый список в аккумуляторе, остальных во второй список. - - -instructions: | - - В функции `filter_adults/1` критерием фильтрации является возраст пользователя. Но этот возраст явно указан в коде функции. Нужно реализовать функцию `filter_by_age/2`, которая принимает список пользователей и возраст. - - ```elixir - users = [ - {:user, 1, "Bob", 23}, - {:user, 2, "Helen", 20}, - {:user, 3, "Bill", 15}, - {:user, 4, "Kate", 14} - ] - - Solution.filter_by_age(users, 16) # [{:user, 1, "Bob", 23}, {:user, 2, "Helen", 20}] - Solution.filter_by_age(users, 14) # [{:user, 1, "Bob", 23}, {:user, 2, "Helen", 20}, {:user, 3, "Bill", 15}] - Solution.filter_by_age(users, 22) # [{:user, 1, "Bob", 23}] - ``` - -tips: - - | - [Статья в Hexlet про рекурсию](https://ru.hexlet.io/blog/posts/recursive) diff --git a/modules/35-fp/20-recursion-with-acc/en/data.yml b/modules/35-fp/20-recursion-with-acc/en/data.yml index 2ca4e46..a6c362a 100644 --- a/modules/35-fp/20-recursion-with-acc/en/data.yml +++ b/modules/35-fp/20-recursion-with-acc/en/data.yml @@ -1,2 +1,3 @@ +--- name: Recursion with accumulator tips: [] diff --git a/modules/35-fp/20-recursion-with-acc/ru/data.yml b/modules/35-fp/20-recursion-with-acc/ru/data.yml index 0a4577e..0eaffe4 100644 --- a/modules/35-fp/20-recursion-with-acc/ru/data.yml +++ b/modules/35-fp/20-recursion-with-acc/ru/data.yml @@ -1,3 +1,4 @@ +--- name: Рекурсивные функции с аккумуляторами tips: - | diff --git a/modules/35-fp/30-immutability/description.en.yml b/modules/35-fp/30-immutability/description.en.yml deleted file mode 100644 index b04acd8..0000000 --- a/modules/35-fp/30-immutability/description.en.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- -name: Immutability -theory: | - - 🚧 In development - -instructions: | - - 🚧 In development - -tips: [] diff --git a/modules/35-fp/30-immutability/description.ru.yml b/modules/35-fp/30-immutability/description.ru.yml deleted file mode 100644 index 839ff9b..0000000 --- a/modules/35-fp/30-immutability/description.ru.yml +++ /dev/null @@ -1,86 +0,0 @@ ---- - -name: Неизменяемые структуры данных -theory: | - - Эликсир, как и большинство других функциональных языков, основан на иммутабельности данных. - - Польза иммутабельности: - - исключает ошибки, связанные с модификацией одной области памяти из разных мест в коде; - - в том числе из разных потоков, что существенно упрощает многопоточное программирование; - - упрощает сборку мусора; - - сохраняются все промежуточные версии данных, что упрощает отладку; - - компилятор имеет больше возможностей для оптимизации кода. - - Плата за это -- несколько менее эффективные структуры данных, чем в императивных языках. Можно говорить, что в среднем производительность мутабельных и иммутабельных структур данных сопоставимая, но в крайних случаях производительность хуже, иногда существенно хуже. - - ## Как это работает - - Допустим, мы создали некую структуру данных, сохранили ее в некой области памяти, и присвоили как значение некой переменной. При этом в переменной сохраняется ссылка на эту область памяти. - - Я могу передавать переменную в функцию. Значение передается по ссылке, а не копируется. Но функция не может изменить это значение. Так что передача по ссылке безопасна. - - Эликсир гарантирует, что выделенная память не модифицируется. Но переменную, которая указывает на эту область памяти, можно изменить. То есть, указать на другую область памяти. - - Для примера рассмотрим код: - - ```elixir - my_list = [1, 2, 3] - # => [1, 2, 3] - my_list = my_list ++ [4, 5] - # => [1, 2, 3, 4, 5] - my_list = my_list ++ [6, 7, 8] - # => [1, 2, 3, 4, 5, 4, 5, 6, 7, 8] - ``` - - Здесь мы имеем 3 списка, каждый из них размещен в своей области памяти. И здесь только одна переменная, которая сперва указывает на первый список, затем на второй, затем на третий. - - ## Переиспользование памяти (Structure Sharing) - - Любые данные нужно как-то модифицировать, иммутабельные данные не исключение. Если каждый раз при модификации мы будем создавать полную копию данных, то это будет очень не эффективно и по использованию памяти, и по использованию CPU. - - Поэтому полного копирования не происходит. Вместо этого, при создании новой структуры данных BEAM переиспользует часть или всю старую структура. Иммутабельность позволяет это делать. - - В примере выше, 3 списка совместно используют одну и ту же область памяти. А новая память выделяется только под новые элементы. Аналогично работает и Map. - - ```elixir - my_map = %{a: 42} - # => %{a: 42} - other_map = Map.put(my_map, :b, 500) - # => %{a: 42, b: 500} - yet_another_map = Map.put(my_map, :c, 100500) - # => %{a: 42, c: 100500} - - my_map - # => %{a: 42} - other_map - # => %{a: 42, b: 500} - yet_another_map - # => %{a: 42, c: 100500} - ``` - -instructions: | - - Реализуем [шифр Цезаря](https://ru.wikipedia.org/wiki/%D0%A8%D0%B8%D1%84%D1%80_%D0%A6%D0%B5%D0%B7%D0%B0%D1%80%D1%8F) -- простой способ шифрования путем сдвига каждого символа на константу. - - Нужно реализовать функцию `encode/2`, которая принимает строку и сдвиг, и возвращает зашифрованную строку. - - ```elixir - Solution.encode('Hello', 10) - # => 'Rovvy' - Solution.encode('Hello', 5) - # => 'Mjqqt' - ``` - - Также нужно реализовать функцию `decode/2`, которая принимает зашифрованную строку и сдвиг, и возвращает оригинальную строку. - - ```elixir - Solution.decode('Rovvy', 10) - # => 'Hello' - Solution.decode('Mjqqt', 5) - # => 'Hello' - ``` - -tips: - - | - [Объяснение иммутабельных структур данных на примере Clojure](https://youtu.be/wASCH_gPnDw?si=l7jkfOLZxDNyu30G&t=1815) diff --git a/modules/35-fp/30-immutability/en/data.yml b/modules/35-fp/30-immutability/en/data.yml index b278cdc..0b42a0c 100644 --- a/modules/35-fp/30-immutability/en/data.yml +++ b/modules/35-fp/30-immutability/en/data.yml @@ -1,2 +1,3 @@ +--- name: Immutability tips: [] diff --git a/modules/35-fp/30-immutability/ru/data.yml b/modules/35-fp/30-immutability/ru/data.yml index 76432ad..6bfb390 100644 --- a/modules/35-fp/30-immutability/ru/data.yml +++ b/modules/35-fp/30-immutability/ru/data.yml @@ -1,3 +1,4 @@ +--- name: Неизменяемые структуры данных tips: - > diff --git a/modules/35-fp/40-anonymous-fn/description.en.yml b/modules/35-fp/40-anonymous-fn/description.en.yml deleted file mode 100644 index 271a2a3..0000000 --- a/modules/35-fp/40-anonymous-fn/description.en.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- -name: Anonymous functions -theory: | - - 🚧 In development - -instructions: | - - 🚧 In development - -tips: [] diff --git a/modules/35-fp/40-anonymous-fn/description.ru.yml b/modules/35-fp/40-anonymous-fn/description.ru.yml deleted file mode 100644 index eb3e8b5..0000000 --- a/modules/35-fp/40-anonymous-fn/description.ru.yml +++ /dev/null @@ -1,54 +0,0 @@ ---- - -name: Анонимные функции -theory: | - - Помимо именованных функций, можно создавать анонимные в форме `fn (x) -> fn_body end`, важно отметить, что для вызова таких функций нужно дописать перед скобками точку `.`. Рассмотрим примеры: - - ```elixir - sum = fn (x, y) -> x + y end - sum.(2, 3) # => 5 - - magic = fn (a, b, c) -> (a + b) * c end - magic.(2, 3, 4) # => 20 - ``` - - Так как функции в эликсире являются объектами первого класса, то часто приходится писать анонимные функции, которые передаются в другие функции и для сокращения записи таких функций используется оператор `&`: - - ```elixir - mul = &(&1 * &2) - mul.(3, 3) # => 9 - - magic = &((&1 + &2 + &3) * &4) - magic.(1, 2, 3, 4) # => 24 - - more_magic = &(&1.(&2)) - increment = &(&1 + 1) - more_magic.(increment, 10) # => 11 - - double = &(&1 * &1) - more_magic.(double, 5) # => 25 - ``` - - При использовании сокращенного синтаксиса для анонимных функций следует быть аккуратным, так как при большом количестве аргументов можно легко запутаться с их порядком, особенно, если в функции есть сложная логика. - -instructions: | - - Даны два *целых* числа. Создайте простой калькулятор, который поддерживает следующие операции: `сложение`, `вычитание`, `деление`, `умножение`. - - ```elixir - Solution.calculate("+", 2, 3) # => 5 - Solution.calculate("+", 0, -3) # => -3 - Solution.calculate("-", 2, 3) # => -1 - Solution.calculate("-", 0, 3) # => -3 - Solution.calculate("/", 4, 4) # => 1.0 - Solution.calculate("/", 3, 2) # => 1.5 - Solution.calculate("*", 2, 2) # => 4 - Solution.calculate("*", 0, 2) # => 0 - ``` - -tips: - - | - [Про объекты первого класса](https://ru.wikipedia.org/wiki/%D0%9E%D0%B1%D1%8A%D0%B5%D0%BA%D1%82_%D0%BF%D0%B5%D1%80%D0%B2%D0%BE%D0%B3%D0%BE_%D0%BA%D0%BB%D0%B0%D1%81%D1%81%D0%B0) - - | - [Почему используются точки при вызове анонимных функций](https://dashbit.co/blog/why-the-dot) diff --git a/modules/35-fp/40-anonymous-fn/en/data.yml b/modules/35-fp/40-anonymous-fn/en/data.yml index 0a7c1e0..092daf8 100644 --- a/modules/35-fp/40-anonymous-fn/en/data.yml +++ b/modules/35-fp/40-anonymous-fn/en/data.yml @@ -1,2 +1,3 @@ +--- name: Anonymous functions tips: [] diff --git a/modules/35-fp/40-anonymous-fn/ru/data.yml b/modules/35-fp/40-anonymous-fn/ru/data.yml index 5925b48..bee0663 100644 --- a/modules/35-fp/40-anonymous-fn/ru/data.yml +++ b/modules/35-fp/40-anonymous-fn/ru/data.yml @@ -1,3 +1,4 @@ +--- name: Анонимные функции tips: - > diff --git a/modules/40-collections/10-map/description.en.yml b/modules/40-collections/10-map/description.en.yml deleted file mode 100644 index c497fb8..0000000 --- a/modules/40-collections/10-map/description.en.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- -name: Map -theory: | - - 🚧 In development - -instructions: | - - 🚧 In development - -tips: [] diff --git a/modules/40-collections/10-map/description.ru.yml b/modules/40-collections/10-map/description.ru.yml deleted file mode 100644 index 1e5d177..0000000 --- a/modules/40-collections/10-map/description.ru.yml +++ /dev/null @@ -1,72 +0,0 @@ ---- - -name: Отображение -theory: | - - Так как язык Эликсир является функциональным языком программирования, то в большинстве случаев списки обрабатываются 'классической' тройкой функций `map`, `filter`, `reduce`. Рассмотрим первую функцию `map`, которая переводится как 'отображение', что полностью отражает суть выполняемой операции: - - ```elixir - numbers = [1, 2, 3, 4] - - doubled_numbers = Enum.map(numbers, fn (x) -> x * 2 end) - IO.puts(doubled_numbers) # => [2, 4, 6, 8] - - users = [%{name: "Igor"}, %{name: "John"}, %{name: "Alice"}, %{name: "Isabella"}] - - names = Enum.map(users, &(&1[:name])) - IO.puts(names) # => ["Igor", "John", "Alice", "Isabella"] - - # либо с помощью паттерн-матчинга - names = Enum.map(users, fn %{name: name} -> name end) - IO.puts(names) # => ["Igor", "John", "Alice", "Isabella"] - ``` - - Важно понимать, что при обработке переданного в функцию `map` списка, создается новый список, а не мутируется исходный, так как язык является иммутабельным. Внутри переданной функции над элементом списка могут выполняться какие-угодно вычисления, однако итоговый список *всегда* будет такого же размера, как и исходный: - - ```elixir - numbers = [1, 2, 3] - - incremented_numbers = Enum.map(numbers, fn (x) -> x + 1 end) - - IO.puts(numbers) # => [1, 2, 3] - IO.puts(incremented_numbers) # => [2, 3, 4] - - stringified = Enum.map(numbers, fn (x) -> "Number: #{x}" end) - IO.puts(stringified) # => ["Number: 1", "Number: 2", "Number: 3"] - ``` - - Так как функция `map` ожидает первым аргументом список, то мы можем с помощью оператора `|>` комбинировать несколько преобразований подряд: - - ```elixir - numbers = [1, 2, 3, 4] - - result = - numbers - |> Enum.map(fn (x) -> x + 1 end) - |> Enum.map(fn (x) -> x * 5 end) - |> Enum.map(fn (x) -> x / 2 end) - |> Enum.map(&Float.round/1) - - IO.puts(result) # => [5.0, 8.0, 10.0, 13.0] - ``` - -instructions: | - Реализуйте функцию `zip`, которая группирует элементы переданных векторов в подвектора. Если вектора отличаются длиной, то вместо сгрупированного элемента оставьте `nil`. Для обращения к элементу списка по индексу используйте `Enum.at`. - Примеры: - - ```elixir - Solution.zip([], []) - # => [] - Solution.zip([1, 2, 3, 4], [5, 6, 7, 8]) - # => [[1, 5], [2, 6], [3, 7], [4, 8]] - Solution.zip([1, 2], [3, 4]) - # => [[1, 3], [2, 4]] - Solution.zip([1, 2], [3]) - # => [[1, 3], [2, nil]] - Solution.zip([1], [3, 4]) - # => [[1, 3], [nil, 4]] - ``` - -tips: - - | - [Про map, filter, reduce](https://ru.hexlet.io/blog/posts/js-prosto-o-slozhnom-filter-map-reduce) diff --git a/modules/40-collections/10-map/en/data.yml b/modules/40-collections/10-map/en/data.yml index 6fcaeba..308325b 100644 --- a/modules/40-collections/10-map/en/data.yml +++ b/modules/40-collections/10-map/en/data.yml @@ -1,2 +1,3 @@ +--- name: Map tips: [] diff --git a/modules/40-collections/10-map/ru/data.yml b/modules/40-collections/10-map/ru/data.yml index 0e2cc34..7d381da 100644 --- a/modules/40-collections/10-map/ru/data.yml +++ b/modules/40-collections/10-map/ru/data.yml @@ -1,3 +1,4 @@ +--- name: Отображение tips: - > diff --git a/modules/40-collections/20-filter/description.en.yml b/modules/40-collections/20-filter/description.en.yml deleted file mode 100644 index 8537406..0000000 --- a/modules/40-collections/20-filter/description.en.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- -name: Filter -theory: | - - 🚧 In development - -instructions: | - - 🚧 In development - -tips: [] diff --git a/modules/40-collections/20-filter/description.ru.yml b/modules/40-collections/20-filter/description.ru.yml deleted file mode 100644 index 34b7f2e..0000000 --- a/modules/40-collections/20-filter/description.ru.yml +++ /dev/null @@ -1,44 +0,0 @@ ---- - -name: Фильтрация -theory: | - - Функция `map` может менять элементы списка, но не может менять их количество: сколько элементов было в исходном списке, столько же останется. Функция `filter` же, напротив, не может менять сами элементы, но может решать, какие из них попадут в выходной список, а какие будут отброшены. - - ```elixir - numbers = [12, 2, 30, 4, 55, 10, 11] - - result = Enum.filter(numbers, fn (x) -> x >= 10 end) - IO.puts(result) # => [12, 30, 55, 10, 11] - - result = - numbers - |> Enum.filter(fn (x) -> x < 20 end) - |> Enum.filter(fn (x) -> rem(x, 2) == 0 end) - |> IO.puts() # => [12, 2, 4, 10] - - users = [%{name: "Igor", age: 21}, %{name: "John", age: 13}, %{name: "Alice", age: 20}, %{name: "Isabella", age: 13}] - - result = Enum.filter(users, fn %{age: age} -> age < 15 end) - IO.puts(result) # => [%{age: 13, name: "John"}, %{age: 13, name: "Isabella"}] - - result = Enum.filter(users, &(String.starts_with?(&1[:name], "Al"))) - IO.puts(result) # => [%{age: 20, name: "Alice"}] - ``` - -instructions: | - Реализуйте функцию `inc_numbers`, которая берёт из переданного списка значения, являющиеся числами `is_number` и возвращает список этих чисел, увеличив каждое число на единицу. - Примеры: - - ```elixir - Solution.inc_numbers(["foo", false, ["foo"]]) - # => [] - Solution.inc_numbers([10, "foo", false, true, ["foo"], 1.2, %{}, 32]) - # => [11, 2.2, 33] - Solution.inc_numbers([1, 2, 3, 4, 5, 6.0]) - # => [2, 3, 4, 5, 6, 7.0] - ``` - -tips: - - | - [Про map, filter, reduce](https://ru.hexlet.io/blog/posts/js-prosto-o-slozhnom-filter-map-reduce) diff --git a/modules/40-collections/20-filter/en/data.yml b/modules/40-collections/20-filter/en/data.yml index 62d2c7a..0275fbf 100644 --- a/modules/40-collections/20-filter/en/data.yml +++ b/modules/40-collections/20-filter/en/data.yml @@ -1,2 +1,3 @@ +--- name: Filter tips: [] diff --git a/modules/40-collections/20-filter/ru/data.yml b/modules/40-collections/20-filter/ru/data.yml index 823c7ba..f80c0ee 100644 --- a/modules/40-collections/20-filter/ru/data.yml +++ b/modules/40-collections/20-filter/ru/data.yml @@ -1,3 +1,4 @@ +--- name: Фильтрация tips: - > diff --git a/modules/40-collections/30-reduce/description.en.yml b/modules/40-collections/30-reduce/description.en.yml deleted file mode 100644 index 3d5f32c..0000000 --- a/modules/40-collections/30-reduce/description.en.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- -name: Reduce -theory: | - - 🚧 In development - -instructions: | - - 🚧 In development - -tips: [] diff --git a/modules/40-collections/30-reduce/description.ru.yml b/modules/40-collections/30-reduce/description.ru.yml deleted file mode 100644 index 656f4af..0000000 --- a/modules/40-collections/30-reduce/description.ru.yml +++ /dev/null @@ -1,66 +0,0 @@ ---- - -name: Свертка -theory: | - - Функции `map` и `filter` обрабатывают списки, сохраняя саму структуру. Иногда нужно избавиться от этой структуры, вычислив какое-то итоговое значение. Простейший пример — сумма всех чисел в списке, либо текст, собранный из списка строк. - - В процедурных языках для получения итоговых значений по списку проходят с использованием цикла и промежуточный результат хранят в отдельной переменной — *аккумуляторе*. - - Декларативным же аналогом такого цикла будет операция *сворачивания* (*folding*) или, как ещё говорят, получение *свёртки* (*fold*). Суть сворачивания списка заключается в последовательном применении некоторой *операции* к очередному элементу списка и текущему значению аккумулятора `acc` с целью получить новое значение аккумулятора. Рассмотрим процесс сворачивания списка `[1, 2, 3, 4]` в сумму чисел. Начальным значением аккумулятора будет `0`, а операцией — `+`. Сложить числа можно как минимум двумя способами: - - 1. двигаясь от первого элемента к последнему, слева-направо: - ``` - (((0 + 1) + 2) + 3) + 4 - ``` - 2. двигаясь от последнего элемента к первому, справа-налево: - ``` - 1 + (2 + (3 + (4 + 0))) - ``` - - Для операции сложения не имеет значения то, какой из вариантов мы выберем. Потому что операция сложения [ассоциативна](https://ru.wikipedia.org/wiki/%D0%90%D1%81%D1%81%D0%BE%D1%86%D0%B8%D0%B0%D1%82%D0%B8%D0%B2%D0%BD%D0%B0%D1%8F_%D0%BE%D0%BF%D0%B5%D1%80%D0%B0%D1%86%D0%B8%D1%8F). Но далеко не все операции таковы: например, при конкатенации строк важно, последнюю мы будем с первой складывать или наоборот. - - ```elixir - Enum.reduce([1, 2, 3], 0, &(&1 + &2)) # => 6 - Enum.reduce([1, 2, 3], 0, fn x, acc -> acc - x end) # => -6 - - Enum.reduce(["a", "b", "c"], "", fn x, acc -> "#{acc}#{x}" end) # => "abc" - Enum.reverse(["a", "b", "c"]) |> Enum.reduce("", fn x, acc -> "#{acc}#{x}" end) # => "cba" - ``` - - Часто используют левую свёртку `reduce` из модуля `Enum` потому, что она более интуитивна — двигается от первого элемента к последнему. Однако, иногда полезна правая. - - Если появилась необходимость использовать правую свертку, то можно обратиться к модулю `List` и функции `foldr`, однако стоит понимать, что в общем случае лучше использовать `Enum`, так как модуль работает со всеми структурами, имплементирующими протокол `Enumerable`, в то время как `List` работает только со списками. Про протоколы будет рассказано чуть позже. - - ```elixir - List.foldr([1, 2, 3], 0, &(&1 + &2)) # => 6 - List.foldr([1, 2, 3], 0, fn x, acc -> acc - x end) # => -6 - - List.foldl(["a", "b", "c"], "", fn x, acc -> "#{acc}#{x}" end) # => "abc" - List.foldr(["a", "b", "c"], "", fn x, acc -> "#{acc}#{x}" end) # => "cba" - ``` - -instructions: | - - Реализуйте функцию `max_delta`, которая должна принимать два списка чисел и вычислять максимальную разницу (абсолютное значение разницы) между соответствующими парами элементов. Примеры: - - ```elixir - Solution.max_delta([], []) - # => 0 - Solution.max_delta([10, -15, 35], [2, -12, 42]) - # => 8 - Solution.max_delta([-5], [-15]) - # => 10 - ``` - - Вам пригодятся функции `abs` и `max`: - - ```elixir - abs(42) # => 42 - abs(-13) # => 13 - max(1, 5) # => 5 - ``` - -tips: - - | - [Про map, filter, reduce](https://ru.hexlet.io/blog/posts/js-prosto-o-slozhnom-filter-map-reduce) diff --git a/modules/40-collections/30-reduce/en/data.yml b/modules/40-collections/30-reduce/en/data.yml index 7c40f14..46f1ec6 100644 --- a/modules/40-collections/30-reduce/en/data.yml +++ b/modules/40-collections/30-reduce/en/data.yml @@ -1,2 +1,3 @@ +--- name: Reduce tips: [] diff --git a/modules/40-collections/30-reduce/ru/data.yml b/modules/40-collections/30-reduce/ru/data.yml index 65ee17e..c52b296 100644 --- a/modules/40-collections/30-reduce/ru/data.yml +++ b/modules/40-collections/30-reduce/ru/data.yml @@ -1,3 +1,4 @@ +--- name: Свертка tips: - > diff --git a/modules/40-collections/40-comprehension/description.en.yml b/modules/40-collections/40-comprehension/description.en.yml deleted file mode 100644 index 3b0f97b..0000000 --- a/modules/40-collections/40-comprehension/description.en.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- -name: Comprehensions -theory: | - - 🚧 In development - -instructions: | - - 🚧 In development - -tips: [] diff --git a/modules/40-collections/40-comprehension/description.ru.yml b/modules/40-collections/40-comprehension/description.ru.yml deleted file mode 100644 index 1b66080..0000000 --- a/modules/40-collections/40-comprehension/description.ru.yml +++ /dev/null @@ -1,97 +0,0 @@ ---- - -name: Компрехеншен или конструкторы списков -theory: | - - Конструкторы списков являются еще одним способом работы с коллекциями, помимо классических `map`, `filter`, `reduce`. - - ```elixir - # пример map - users = [%{name: "Igor", age: 21}, %{name: "John", age: 13}, %{name: "Alice", age: 20}, %{name: "Isabella", age: 13}] - - for %{name: name, age: age} <- users, do: "Name: #{name}, age: #{age}" - # => ["Name: Igor, age: 21", "Name: John, age: 13", "Name: Alice, age: 20", "Name: Isabella, age: 13"] - - # пример filter - for %{age: age} = user <- users, age > 15, do: user - # => [%{age: 21, name: "Igor"}, %{age: 20, name: "Alice"}] - - # комбинация map и filter - for %{name: name, age: age} <- users, age < 15, do: name - # => ["John", "Isabella"] - ``` - - Конструкторы списков могут обрабатывать несколько списков одновременно: - - ```elixir - list1 = [1, 2, 3] - list2 = [:a, :b, :c] - - for x <- list1, y <- list2, do: {x, y} - # => [ - # => {1, :a}, - # => {1, :b}, - # => ... - # => {3, :b}, - # => {3, :c} - # => ] - ``` - - Элементы списков соединяются "каждый с каждым". - - Коллекция может быть любой структурой данных, реализующей протокол `Enumerable`. Каждый элемент коллекции сопоставляется с шаблоном, и если не происходит совпадения, то этот элемент отбрасывается. - - ```elixir - # пример map - users = [%{name: "Igor"}, %{name: "John", age: 13}, %{name: "Alice"}, %{name: "Isabella", age: 13}] - - for %{name: name, age: age} <- users, do: "Name: #{name}, age: #{age}" - # => ["Name: John, age: 13", "Name: Isabella, age: 13"] - ``` - - В конструктор списков можно передать опцию `into`, которая позволяет добавить результат генератора списка в существующую коллекцию. - - ```elixir - result = %{a: 1, b: 2} - - for {k, v} <- [{:c, 3}, {:d, 4}, {:name, "Igor"}], into: result, do: {k, v} - # => %{a: 1, b: 2, c: 3, d: 4, name: "Igor"} - ``` - -instructions: | - - Создайте функцию `fetch_gamers`, которая принимает список сотрудников и выводит список активных сотрудников (статус `:active`) сотрудников у которых хобби связаны с играми (тип хобби `:gaming`). Структура сотрудников описана в примере: - - ```elixir - employees = [ - %{ - name: "Eric", - status: :active, - hobbies: [%{name: "Text Adventures", type: :gaming}, %{name: "Chickens", type: :animals}] - }, - %{ - name: "Mitch", - status: :former, - hobbies: [%{name: "Woodworking", type: :making}, %{name: "Homebrewing", type: :making}] - }, - %{ - name: "Greg", - status: :active, - hobbies: [ - %{name: "Dungeons & Dragons", type: :gaming}, - %{name: "Woodworking", type: :making} - ] - } - ] - - - Solution.fetch_gamers(employees) - # => [ - # => {"Eric", %{name: "Text Adventures", type: :gaming}}, - # => {"Greg", %{name: "Dungeons & Dragons", type: :gaming}} - # => ] - ``` - -tips: - - | - [Официальная документация](https://hexdocs.pm/elixir/Kernel.SpecialForms.html#for/1) diff --git a/modules/40-collections/40-comprehension/en/data.yml b/modules/40-collections/40-comprehension/en/data.yml index 6a4b484..c2c0970 100644 --- a/modules/40-collections/40-comprehension/en/data.yml +++ b/modules/40-collections/40-comprehension/en/data.yml @@ -1,2 +1,3 @@ +--- name: Comprehensions tips: [] diff --git a/modules/40-collections/40-comprehension/ru/data.yml b/modules/40-collections/40-comprehension/ru/data.yml index 75c1c0a..9c450c9 100644 --- a/modules/40-collections/40-comprehension/ru/data.yml +++ b/modules/40-collections/40-comprehension/ru/data.yml @@ -1,3 +1,4 @@ +--- name: Компрехеншен или конструкторы списков tips: - > diff --git a/modules/40-collections/50-stream/description.en.yml b/modules/40-collections/50-stream/description.en.yml deleted file mode 100644 index 64ee2a2..0000000 --- a/modules/40-collections/50-stream/description.en.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- -name: Streams -theory: | - - 🚧 In development - -instructions: | - - 🚧 In development - -tips: [] diff --git a/modules/40-collections/50-stream/description.ru.yml b/modules/40-collections/50-stream/description.ru.yml deleted file mode 100644 index ae6fd18..0000000 --- a/modules/40-collections/50-stream/description.ru.yml +++ /dev/null @@ -1,142 +0,0 @@ ---- - -name: Стримы -theory: | - - Идея ленивых вычислений состоит в том, что у нас есть некая функция, результат которой мы хотим вычислить не сразу, а чуть позже, когда будет нужно. Как это может быть полезно? Рассмотрим на примерах. - - В большинстве языков программирования вычисления происходят в момент их описания, а к примеру, в Haskell, все вычисления по умолчанию ленивы. - - Наивная реализация ленивых вычислений - анонимная функция, сохраненная в переменную, вызов которой сделаем дальше по коду, основная сложность возникает, когда мы хотим сделать вызов комбинации из таких "ленивых функций". Для этого есть модуль `Stream`. В нем есть такие же функции работы с коллекциями как `map`, `filter`, `reduce`, но с некоторыми отличиями, которые мы рассмотрим дальше: - - ```elixir - numbers = [1,2,3] - - numbers - |> Enum.map(&(&1 * &1)) - |> Enum.zip([:a, :b, :c]) - |> Enum.filter(fn({n, s}) -> n > 2 and s != :c end) - # => [{4, :b}] - ``` - - В этом случае мы прошлись три раза по всему списку, так как промежуточные результаты передаются дальше по цепочке вызовов функций. Если бы мы обходили список в каком-нибудь императивном языке, то использовали бы примерно в 3 раза меньше памяти и времени CPU. Теперь используем модуль `Stream`: - - ```elixir - numbers = [1,2,3] - - numbers - |> Stream.map(&(&1 * &1)) - |> Stream.zip([:a, :b, :c]) - |> Stream.filter(fn({n, s}) -> n > 2 and s != :c end) - |> Enum.to_list - # => [{4, :b}] - ``` - - Результат остался тем же, только в конце добавили функцию `Enum.to_list`, которая запускает цепочку ленивых вычислений. Без него результат был бы таким: - - ```elixir - numbers = [1, 2, 3] - - numbers - |> Stream.map(&(&1 * &1)) - |> Stream.zip([:a, :b, :c]) - |> Stream.filter(fn({n, s}) -> n > 2 and s != :c end) - # => #Stream<[ - # => enum: #Function<73.29975488/2 in Stream.zip_with/2>, - # => funs: [#Function<40.29975488/1 in Stream.filter/2>] - # => ]> - ``` - - Мы получили некую структуру данных, в ней описаны вычисления, которые будут применены к этой структуре при вычислении. - - Так как это структура данных, мы можем ее положить в переменную и когда понадобится, вычислить вызовом соответствующей функции, например `Enum.to_list`. Но самое важное, проход по списку в этом случае будет лишь один раз, то есть результатов промежуточных вычислений не будет, что уменьшает количество потребляемой памяти и времени процессора. - - ```elixir - Enum.map(1..10_000_000, &(&1 + 1)) |> Enum.take(5) - # => [2, 3, 4, 5, 6] - - Stream.map(1..10_000_000, &(&1 + 1)) |> Enum.take(5) - # => [2, 3, 4, 5, 6] - ``` - - Второй случай вычислится быстрее. Почему? В первом случае вычислится *весь* список, во втором только нужная часть списка, а точнее первые пять элементов, именно так `Stream` и работает. - - Возьмем еще один пример. Допустим у нас есть большой файл, который содержит всю текстовую часть статей из Википедии, который весит примерно 20 Гб и мы хотим в нем найти самое длинное слово. Реализуем через `Enum`: - - ```elixir - File.read!("data/wiki.txt") - |> String.split("\n") - |> Enum.map(fn(line) -> String.split(line, " ") end) - |> Enum.map(fn(words) -> ... - |> Enum.max_by - ``` - - В этом случае мы загрузим в оперативную память весь файл, который весит 20 Гб, так еще и пройдемся по нему 4 раза! - - Теперь воспользуемся `Stream`: - - ```elixir - File.stream!("data/wiki.txt") - |> Stream.map(fn(line) -> String.split(line, " ") end) - |> Stream.map(fn(words) -> ... - |> Enum.max_by - ``` - - Функция `File.stream!` возвращает ленивую коллекцию, которая построчно читает файл. В этом случае мы загружаем в оперативную память данные небольшими порциями и выполняем только один проход по ним. Последний вызов `Enum.max_by` запускает вычисления. Не факт, что этот код будет быстрее, так как мы много раз обращаемся к диску, но он точно расходует намного меньше оперативной памяти. - - Иногда коллекции бывает *бесконечными*. На такой случай `Stream` имеет функции `cycle`, `repeatedly`, `iterate`, `unfold`, `resource`. Рассмотрим `cycle` и `unfold`. - - `Stream.cycle` принимает коллекцию и генерирует бесконечную коллекцию из повторений элементов переданной: - - ```elixir - Stream.cycle([1, 2, 3, 4, 5]) |> Enum.take(20) - # => [1, 2, 3, 4, 5, 1, 2, 3, 4, 5, 1, 2, 3, 4, 5, 1, 2, 3, 4, 5] - ``` - - Это можно использовать, например для генерации HTML элементов: - - ```elixir - texts = ["foo", "bar", "hello", "hexlet"] - - Stream.cycle(["red", "black", "green"]) - |> Stream.zip(texts) - |> Enum.map(fn {color, text} -> "
#{text}
" end) - - # => ["foo
", "bar
", "hello
", "hexlet
"] - ``` - - `Stream.unfold` можно представить, как функцию, обратную *свертке* (fold или reduce). Если fold сворачивает список в одиночное значение, то unfold наоборот, из одиночного значения разворачивает список. - - Функция принимает на вход начальное значение и разворачивающую функцию. Разворачивающая функция принимает на вход текущее состояние, и возвращает кортеж из двух значений. Первый элемент кортежа, это то, что становится очередным элементом списка. Второй элемент кортежа, это новое состояние, которое передается в разворачивающую функцию на следующем шаге. - - Рассмотрим пример с числами Фибоначчи: - - ```elixir - initial_state = {0, 1} - - unfolder = fn({val1, val2}) -> - curr_val = val1 - new_state = {val2, val1 + val2} - {curr_val, new_state} - end - - Stream.unfold(initial_state, unfolder) |> Enum.take(20) - # => [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181] - ``` - -instructions: | - - Создайте функцию `generate`, которая случайно генерирует бесконечную последовательность чисел от 1 до 20 и берет `n` элементов от этой коллекции: - - ```elixir - Solution.generate(5) - # => [2, 10, 20, 12, 11] - Solution.generate(2) - # => [7, 14] - Solution.generate(20) - # => [2, 9, 18, 1, 3, 16, 1, 1, 20, 20, 9, 11, 10, 14, 10, 15, 3, 3, 10, 8] - ``` - -tips: - - | - [Официальная документация](https://hexdocs.pm/elixir/Stream.html) diff --git a/modules/40-collections/50-stream/en/data.yml b/modules/40-collections/50-stream/en/data.yml index e130ff4..0cc63ea 100644 --- a/modules/40-collections/50-stream/en/data.yml +++ b/modules/40-collections/50-stream/en/data.yml @@ -1,2 +1,3 @@ +--- name: Streams tips: [] diff --git a/modules/40-collections/50-stream/ru/data.yml b/modules/40-collections/50-stream/ru/data.yml index fde1903..ed6d4c7 100644 --- a/modules/40-collections/50-stream/ru/data.yml +++ b/modules/40-collections/50-stream/ru/data.yml @@ -1,3 +1,4 @@ +--- name: Стримы tips: - | diff --git a/modules/45-structs/10-struct-intro/description.en.yml b/modules/45-structs/10-struct-intro/description.en.yml deleted file mode 100644 index 1362aeb..0000000 --- a/modules/45-structs/10-struct-intro/description.en.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- -name: Structs intro -theory: | - - 🚧 In development - -instructions: | - - 🚧 In development - -tips: [] diff --git a/modules/45-structs/10-struct-intro/description.ru.yml b/modules/45-structs/10-struct-intro/description.ru.yml deleted file mode 100644 index 42bbfab..0000000 --- a/modules/45-structs/10-struct-intro/description.ru.yml +++ /dev/null @@ -1,156 +0,0 @@ ---- -name: Введение в структуры -theory: | - - Для моделирования данных, которые лучше отражает предметную область, используют структуры. - - Структуры по сути словари, но с некоторыми особенностями. Структуры проверяются на уровне компиляции, в них можно задать значения по умолчанию и структурам недоступны протоколы для словарей, например протоколы `Enum`. Протоколы рассмотрим чуть дальше, а сейчас изучим структуры: - - ```elixir - defmodule Counter do - defstruct state: 0 - end - - my_counter = %Counter{} - %Counter{state: 0} - ``` - - В примере мы объявили модуль `Counter` и в нем объявили, что этот модуль является структурой с полем `state`, которое по умолчанию равно `0`. Затем мы связали `my_counter` с ранее объявленной структурой. Еще мы можем объявить поля без значения по умолчанию: - - ```elixir - defmodule Counter do - defstruct [:current, initial: 0, hello: "world"] - end - - defmodule Counter2 do - defstruct current: nil, initial: 0, hello: "world" - end - - my_counter = %Counter{} - # => %Counter{current: nil, initial: 0, hello: "world"} - my_second_counter = %Counter2{} - # => %Counter2{current: nil, initial: 0, hello: "world"}} - ``` - - Теперь рассмотрим, как работать с полями структур: - - ```elixir - defmodule User do - defstruct age: 0, name: "John" - end - - john = %User{age: 20} - john.age - # => 20 - - john.name - # => "John" - - alice = %{john | name: "Alice"} - alice.name - # => "Alice" - - jane = %{alice | other_field: "some"} - # => ** (KeyError) key :other_field not found in: %User{age: 20, name: "Alice"} - # => (stdlib 4.3.1.1) :maps.update(:other_field, "some", %User{age: 20, name: "Alice"}) - ``` - - Используя синтаксис модификации `|`, виртуальная машина заранее знает, что структура имеет фиксированное количество полей и не дает добавлять новые, только менять заранее объявленные. Интересный момент, что структуры можно использовать в паттерн-матчинге: - - ```elixir - defmodule Pet do - defstruct name: "Barkley" - end - - defmodule User do - defstruct name: "John" - end - - defmodule Example do - def print_name(%User{name: name} = user) do - "Hello, human #{name}" - end - - def print_name(%Pet{name: name} = pet) do - "Hello, pet #{name}" - end - end - - pet = %Pet{} - user = %User{} - - Example.print_name(pet) - # => "Hello, pet Barkley" - Example.print_name(user) - # => "Hello, human John" - ``` - - Благодаря паттерн-матчингу мы добились полиморфного поведения функции. - - Так как структуры не используют протоколы словарей, то и доступ к полю через `[]` становится недоступным: - - ```elixir - defmodule User do - defstruct age: 0, name: "John" - end - - user = %User{} - # => %User{age: 0, name: "John"} - - user.name - # => "John" - - user[:name] - # => ** (UndefinedFunctionError) function User.fetch/2 is undefined (User does not implement the Access behaviour. If you are using get_in/put_in/update_in, you can specify the field to be accessed using Access.key!/1) - # => User.fetch(%User{age: 0, name: "John"}, :name) - # => (elixir 1.15.0) lib/access.ex:305: Access.get/3 - # => iex:201: (file) - - Map.get(user, :name) - # => "John" - ``` - - Еще с помощью атрибута `enforce_keys` можно добавить проверку на инициализацию данных по ключу при создании структуры: - - ```elixir - defmodule User do - @enforce_keys [:name, :age] - defstruct [:name, :age] - end - - %User{} - # => ** (ArgumentError) the following keys must also be given when building struct User: [:name, :age] - # => expanding struct: User.__struct__/1 - # => iex:204: (file) - - %User{name: "John"} - # => ** (ArgumentError) the following keys must also be given when building struct User: [:age] - # => expanding struct: User.__struct__/1 - # => iex:205: (file) - - %User{name: "John", age: 20} - # => %User{name: "John", age: 20} - ``` - - Такая проверка работает только во время компиляции и только при создании структуры. Проверка не будет запускаться при изменении данных по ключу. - -instructions: | - - Создайте функцию `calculate_stats`, которая подсчитывает, сколько в списке людей и питомцев: - - ```elixir - users_and_pets = [%User{}, %User{}, %Pet{}] - - Solution.calculate_stats(users_and_pets) - # => %{humans: 2, pets: 1} - - Solution.calculate_stats([]) - # => %{humans: 0, pets: 0} - - only_pets = [%Pet{}, %Pet{}, %Pet{}] - # => %{humans: 0, pets: 3} - ``` - - Обратите внимание, что структуры в модуле заранее определены. - -tips: [] diff --git a/modules/45-structs/10-struct-intro/en/data.yml b/modules/45-structs/10-struct-intro/en/data.yml index 2a1e307..49df246 100644 --- a/modules/45-structs/10-struct-intro/en/data.yml +++ b/modules/45-structs/10-struct-intro/en/data.yml @@ -1,2 +1,3 @@ +--- name: Structs intro tips: [] diff --git a/modules/45-structs/10-struct-intro/ru/data.yml b/modules/45-structs/10-struct-intro/ru/data.yml index 64f4617..11d7c84 100644 --- a/modules/45-structs/10-struct-intro/ru/data.yml +++ b/modules/45-structs/10-struct-intro/ru/data.yml @@ -1,2 +1,3 @@ +--- name: Введение в структуры tips: [] diff --git a/modules/45-structs/20-typespecs/description.en.yml b/modules/45-structs/20-typespecs/description.en.yml deleted file mode 100644 index 8ebc2f4..0000000 --- a/modules/45-structs/20-typespecs/description.en.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- -name: Typespecs -theory: | - - 🚧 In development - -instructions: | - - 🚧 In development - -tips: [] diff --git a/modules/45-structs/20-typespecs/description.ru.yml b/modules/45-structs/20-typespecs/description.ru.yml deleted file mode 100644 index 15a5242..0000000 --- a/modules/45-structs/20-typespecs/description.ru.yml +++ /dev/null @@ -1,109 +0,0 @@ ---- -name: Описание типов -theory: | - - Для продолжения обсуждения `протоколов` и `поведения`, нам нужно ознакомиться с типизацией в Elixir. - - Elixir - динамически типизированный язык, поэтому все типы в Elixir проверяются во время выполнения. Тем не менее, Elixir поставляется со *спецификациями*, которые представляют собой нотацию, используемую для: - - - объявления типизированных сигнатур функций (также называемых спецификациями); - - объявления пользовательских типов. - - Спецификации полезны для документации кода и статического анализа кода, например, Dialyzer в Erlang или Dialyxir для Elixir. - - Elixir предоставляет множество встроенных типов, таких как `integer` или `pid`, которые используются для документирования сигнатур функций. Например, функция round/1, которая округляет число до ближайшего целого. Как видно из документации, [типизированная сигнатура функции round/1](https://hexdocs.pm/elixir/Kernel.html#round/1) имеет вид: - - ```elixir - round(number()) :: integer() - ``` - - Синтаксис состоит в том, что слева от :: указывается функция и ее входные данные, а справа - тип возвращаемого значения. Типы могут не содержать круглых скобок. - - В коде спецификации функций записываются с помощью атрибута `@spec`, который располагается перед определением функции. В спецификациях могут описываться как публичные, так и частные функции. Имя функции и количество аргументов, используемых в атрибуте `@spec`, должны соответствовать описываемой функции: - - ```elixir - defmodule Example do - - @spec substract(integer, integer) :: integer - def substract(first, second) do - first - second - end - - @spec inc_list(list(integer)) :: list(integer) - def inc_list(numbers) do - numbers |> Enum.map(&(&1 + 1)) - end - - @spec magic(boolean) :: integer | String.t - def magic(do_magic \\ true) do - if do_magic, do: 2, else: "hello" - end - end - ``` - - Определение пользовательских типов может помочь передать замысел вашего кода и повысить его читаемость. Пользовательские типы могут быть определены в модулях с помощью атрибута `@type`. - - Примером реализации пользовательского типа является предоставление более описательного псевдонима существующего типа. Например, определение `year` в качестве типа делает спецификации функций более описательными, чем если бы они использовали просто `integer`: - - ```elixir - defmodule User do - @typedoc """ - A 4 digit year, e.g. 1984 - """ - @type year :: integer - - @spec current_age(year) :: integer - def current_age(year_of_birth), do: # implementation - end - ``` - - Пользовательские типы могут быть и сложнее, например: - - ```elixir - @type error_map :: %{ - message: String.t, - line_number: integer - } - ``` - - Пользовательские типы можно использовать внутри других модулей: - - ```elixir - defmodule Errors do - @type error_map :: %{ - message: String.t, - line_number: integer - } - end - - defmodule Magic do - - @spec do_magic(integer) :: integer | Errors.error_map - def do_magic(a) when is_integer(a) do - Enum.random(1..100) - end - - def do_magic(_) do - %{message: "Incorrect argument", line_number: Enum.random(1..100)} - end - end - ``` - - -instructions: | - - Создайте функцию `generate_pets`, в которую передается количество питомцев, список которых нужно сгенерировать с именем `Barkley x`, где `x` - идентификатор питомца (отсчет идет с нуля). Модуль `Pet` описан заранее. Опишите спецификацию созданной функции: - - ```elixir - Solution.generate_pets(2) - # => [%Pet{name: "Barkley 0"}, %Pet{name: "Barkley 1"}] - - Solution.generate_pets(-2) - # => [] - ``` - -tips: - - | - [Список типов](https://hexdocs.pm/elixir/typespecs.html#basic-types) - - | - [Dyalyzer для Elixir](https://github.com/jeremyjh/dialyxir) diff --git a/modules/45-structs/20-typespecs/en/data.yml b/modules/45-structs/20-typespecs/en/data.yml index 41c3fcb..8480cff 100644 --- a/modules/45-structs/20-typespecs/en/data.yml +++ b/modules/45-structs/20-typespecs/en/data.yml @@ -1,2 +1,3 @@ +--- name: Typespecs tips: [] diff --git a/modules/45-structs/20-typespecs/ru/data.yml b/modules/45-structs/20-typespecs/ru/data.yml index 2d5b893..691f8d2 100644 --- a/modules/45-structs/20-typespecs/ru/data.yml +++ b/modules/45-structs/20-typespecs/ru/data.yml @@ -1,3 +1,4 @@ +--- name: Описание типов tips: - | diff --git a/modules/45-structs/30-behaviour/description.en.yml b/modules/45-structs/30-behaviour/description.en.yml deleted file mode 100644 index c3c34a2..0000000 --- a/modules/45-structs/30-behaviour/description.en.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- -name: Behaviours -theory: | - - 🚧 In development - -instructions: | - - 🚧 In development - -tips: [] diff --git a/modules/45-structs/30-behaviour/description.ru.yml b/modules/45-structs/30-behaviour/description.ru.yml deleted file mode 100644 index bcb4cab..0000000 --- a/modules/45-structs/30-behaviour/description.ru.yml +++ /dev/null @@ -1,125 +0,0 @@ ---- -name: Поведение -theory: | - - Многие модули используют один и тот же общедоступный API. [Рассмотрим Plug](https://hexdocs.pm/plug/readme.html), который, как сказано в его описании, является спецификацией для композитных (composable) модулей в веб-приложениях. Каждый модуль, *поведение* которого соответствует Plug, должен реализовать как минимум две публичные функции: `init/1` и `call/2`. - - Поведения (behaviour) предоставляют возможность: - - определить набор функций, которые должны быть реализованы модулем; - - гарантировать, что модуль реализует все функции из этого набора. - - По сути, поведение это как интерфейсы в объектно-ориентированных языках типа Java: набор сигнатур функций, которые должен реализовать модуль. В отличие от протоколов, которые мы рассмотрим в следующем модуле, поведение не зависит от типа данных. - - Теперь определим поведение на примере. Допустим, мы хотим реализовать несколько парсеров, каждый из которых будет разбирать структурированные данные: например, парсер JSON и парсер MessagePack. Каждый из этих двух парсеров будет вести себя одинаково: оба будут предоставлять функцию `parse/1` и функцию `extensions/0`. Функция `parse/1` будет возвращать Elixir-представление структурированных данных, а функция `extensions/0` - список расширений файлов, используются для каждого типа данных (например, .json для файлов JSON). - - ```elixir - defmodule Parser do - @doc """ - Parses a string. - """ - @callback parse(String.t) :: {:ok, any} | {:error, atom} - - @doc """ - Lists all supported file extensions. - """ - @callback extensions() :: [String.t] - end - ``` - - Модули, использующие поведение `Parser`, должны реализовать все функции, определенные с помощью атрибута @callback. Как видно, @callback ожидает не только имя функции, но и спецификацию функции, подобную той, что используется с атрибутом @spec, рассмотренные в прошлом модуле. Также обратите внимание, что для представления разобранного значения используется тип `any`. - - Теперь реализуем описанное поведение: - - ```elixir - defmodule JSONParser do - @behaviour Parser - - @impl Parser - def parse(str), do: {:ok, "parsed json " <> str} - - @impl Parser - def extensions, do: [".json"] - end - - defmodule CSVParser do - @behaviour Parser - - @impl Parser - def parse(str), do: {:ok, "parsed csv " <> str} - - @impl Parser - def extensions, do: [".csv"] - end - ``` - - Если модуль, реализующий заданное поведение, не реализует одну из функций обратного вызова (callback), требуемых этим поведением, то будет выведено предупреждение на этапе компиляции. - - Кроме того, с помощью `@impl` можно убедиться в том, что вы реализуете правильные функции обратного вызова из заданного поведения в явном виде. Например, следующий парсер реализует и `parse`, и `extensions`. Однако, из-за опечатки, BADParser реализует `parse/0` вместо `parse/1`: - - ```elixir - defmodule BADParser do - @behaviour Parser - - @impl Parser - def parse, do: {:ok, "oh no"} - - @impl Parser - def extensions, do: ["boom"] - end - ``` - - При компиляции этого кода, компилятор выдает предупреждение о том, что реализован `parse/0`, а не `parse/1`. - - Поведения полезны тем, что можно передавать модули в качестве аргументов и затем вызывать любую из функций, указанных в поведении. Например, у нас может быть функция, которая получает имя файла, и на основе его расширения вызывает соответствующий парсер: - - ```elixir - @spec parse_file(Path.t(), [module()]) :: {:ok, any} | {:error, atom} - def parse_file(filename, parsers) do - with {:ok, ext} <- parse_extension(filename), - {:ok, parser} <- find_parser(ext, parsers), - {:ok, contents} <- File.read(filename) do - parser.parse(contents) - end - end - - defp parse_extension(filename) do - if ext = Path.extname(filename) do - {:ok, ext} - else - {:error, :no_extension} - end - end - - defp find_parser(ext, parsers) do - if parser = Enum.find(parsers, fn parser -> ext in parser.extensions() end) do - {:ok, parser} - else - {:error, :no_matching_parser} - end - end - ``` - - Можно вызвать необходимый парсер напрямую или сделать словарь, где ключем будет нужное расширение файла, а значением нужный модуль парсера, однако *поведение* в этом дает чуть больше гарантий, что функции модулей реализованы соответствующим образом. - -instructions: | - - Реализуйте парсер, который читает текст (расширение `.txt`) и построчно читает его (разделитель `\n`). Если текст пустой, верните ошибку: - - ```elixir - TextParser.extensions() - # => [".txt"] - - text = "hello\nworld!" - TextParser.parse(text) - # => {:ok, ["hello", "world!"]} - - text = "" - TextParser.parse(text) - # => {:error, :no_text} - ``` - -tips: - - | - [Официальная документация](https://hexdocs.pm/elixir/Module.html#module-behaviour) - - | - [Документация Plug](https://hexdocs.pm/plug/readme.html) diff --git a/modules/45-structs/30-behaviour/en/data.yml b/modules/45-structs/30-behaviour/en/data.yml index 7b0b598..a1446ec 100644 --- a/modules/45-structs/30-behaviour/en/data.yml +++ b/modules/45-structs/30-behaviour/en/data.yml @@ -1,2 +1,3 @@ +--- name: Behaviours tips: [] diff --git a/modules/45-structs/30-behaviour/ru/data.yml b/modules/45-structs/30-behaviour/ru/data.yml index 805f297..d630460 100644 --- a/modules/45-structs/30-behaviour/ru/data.yml +++ b/modules/45-structs/30-behaviour/ru/data.yml @@ -1,3 +1,4 @@ +--- name: Поведение tips: - > diff --git a/modules/45-structs/40-protocols/description.en.yml b/modules/45-structs/40-protocols/description.en.yml deleted file mode 100644 index bf544b4..0000000 --- a/modules/45-structs/40-protocols/description.en.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- -name: Protocols -theory: | - - 🚧 In development - -instructions: | - - 🚧 In development - -tips: [] diff --git a/modules/45-structs/40-protocols/description.ru.yml b/modules/45-structs/40-protocols/description.ru.yml deleted file mode 100644 index c668490..0000000 --- a/modules/45-structs/40-protocols/description.ru.yml +++ /dev/null @@ -1,115 +0,0 @@ ---- -name: Протоколы -theory: | - - С помощью протоколов в Elixir реализуется *полиморфное поведение* в зависимости от типа данных. Начнем с примера реализации полиморфного поведения с помощью сопоставления с образцом: - - ```elixir - defmodule Typer do - def type(value) when is_binary(value), do: "string" - def type(value) when is_integer(value), do: "integer" - def type(value) when is_list(value), do: "array" - def type(value) when is_map(value), do: "hash" - # ... other implementations - end - - Typer.type(2) - # => "integer" - - Typer.type([]) - # => "array" - - Typer.type(%{}) - # => "hash" - ``` - - С этим кодом не будет проблем, если он используется в рамках одного приложения в небольшом количестве мест. Однако, поддерживать такой код станет заметно сложнее, если он превратится в *зависимость* для нескольких приложений (см. expression problem), так как нет простых способов расширять функциональность такого модуля. - - Как раз для таких случаев и используются *протоколы*. По сути протоколы позволяют выполнять диспетчеризацию на *любом* типе данных, который реализуют соответствующий протокол. Важно, что протоколы могут быть реализованы в любом месте, где это нужно. Теперь перепишем модуль `Typer`: - - ```elixir - defprotocol Typer do - @spec type(t) :: String.t() - def type(value) - end - - defimpl Typer, for: BitString do - def type(_value), do: "string" - end - - defimpl Typer, for: Integer do - def type(_value), do: "integer" - end - - defimpl Typer, for: List do - def type(_value), do: "array" - end - - defimpl Typer, for: Map do - def type(_value), do: "hash" - end - - Typer.type(2) - # => "integer" - - Typer.type([]) - # => "array" - - Typer.type(%{}) - # => "hash" - ``` - - Мы определили протокол с помощью `defprotocol` - описали в нем функцию со спецификацией. Затем через `defimpl` мы описали реализацию протокола для соответствующих типов данных. - - С помощью протоколов мы получили преимущество. Теперь мы не привязаны к модулю, в котором определили протокол, если нужно добавить реализацию для какого-либо нового типа. Например, реализацию протокола `Typer` мы можем разнести по разным файлам и модулям, а Elixir найдет и вызовет нужную реализацию протокола для описанных нами данных. - - Функции, определенные в протоколе, могут принимать больше одного аргумента, но *диспетчеризация произойдет по первому аргументу*. - - Протоколы можно определить для всех типов Elixir: - - Atom - - BitString - - Float - - Function - - Integer - - List - - Map - - PID - - Port - - Reference - - Tuple - - Помимо встроенных типов, протоколы можно определять для *структур*: - - ```elixir - defmodule User do - defstruct [:name, :age] - end - - defimpl Typer, for: User do - def type(_value), do: "user" - end - - Typer.type(%User{age: 20, name: "John"}) - # => "user" - ``` - -instructions: | - - Определите три структуры `Human`, `Dog` и `Cat` с полем `name`. Затем определите функцию `say_something` для протокола `Teller` для каждого из модулей, которая возвращает строку в зависимости от типа: - - Для Human `Hello, world!` - - Для Cat `Meow, world!` - - Для Dog `Bark, world!` - - ```elixir - Teller.say_something(%Human{name: "John"}) # => "Hello, world!" - Teller.say_something(%Dog{name: "Barkinson"}) # => "Bark, world!" - Teller.say_something(%Cat{name: "Meowington"}) # => "Meow, world!" - ``` - -tips: - - | - [Официальная документация](https://hexdocs.pm/elixir/Protocol.html) - - | - [Про Expression Problem](https://en.wikipedia.org/wiki/Expression_problem) - - | - [Подкаст про Expression Problem](https://soundcloud.com/mimpod/episode_61) diff --git a/modules/45-structs/40-protocols/en/data.yml b/modules/45-structs/40-protocols/en/data.yml index 83119c5..9c45944 100644 --- a/modules/45-structs/40-protocols/en/data.yml +++ b/modules/45-structs/40-protocols/en/data.yml @@ -1,2 +1,3 @@ +--- name: Protocols tips: [] diff --git a/modules/45-structs/40-protocols/ru/data.yml b/modules/45-structs/40-protocols/ru/data.yml index 0a1816c..06ffb3c 100644 --- a/modules/45-structs/40-protocols/ru/data.yml +++ b/modules/45-structs/40-protocols/ru/data.yml @@ -1,3 +1,4 @@ +--- name: Протоколы tips: - | diff --git a/modules/45-structs/50-any-protocols/description.en.yml b/modules/45-structs/50-any-protocols/description.en.yml deleted file mode 100644 index 5593026..0000000 --- a/modules/45-structs/50-any-protocols/description.en.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- -name: Any protocols -theory: | - - 🚧 In development - -instructions: | - - 🚧 In development - -tips: [] diff --git a/modules/45-structs/50-any-protocols/description.ru.yml b/modules/45-structs/50-any-protocols/description.ru.yml deleted file mode 100644 index c675aee..0000000 --- a/modules/45-structs/50-any-protocols/description.ru.yml +++ /dev/null @@ -1,72 +0,0 @@ ---- -name: Общие протоколы -theory: | - - Ручная реализация протоколов для всех типов может быстро стать повторяющейся и утомительной. В таких случаях Elixir предоставляет два варианта: можно явно вывести (derive) реализацию протокола для наших типов или автоматически реализовать протокол для всех типов. В обоих случаях необходимо реализовать протокол для `Any`. - - Опишем явное выведение (derive) протокола из примера `Typer` прошлого модуля: - - ```elixir - defprotocol Typer do - @spec type(t) :: String.t() - def type(value) - end - - defimpl Typer, for: Any do - def type(_value), do: "something" - end - ``` - - Конечно, реализация получилось странной, потому что для любого типа будет возвращаться `something`, однако это учебный пример. Теперь опишем пользователя и явно *выведем* для него протокол: - - ```elixir - defmodule SomeUser do - @derive [Typer] - defstruct [:name, :age] - end - - Typer.type(%SomeUser{}) - # => "something" - ``` - - В другом случае, в протоколе указывается опция `fallback_to_any` и если не будет найден нужный протокол под тип, то будет использована реализация протокола для `Any`: - - ```elixir - defprotocol Typer do - @fallback_to_any true - - def type(value) - end - - defimpl Typer, for: Any do - def type(_value), do: "something new" - end - - defmodule AnotherUser do - defstruct [:name] - end - - Typer.type(%AnotherUser{}) - # => "something new" - ``` - - Как было сказано выше, не всегда реализация протокола для `Any` необходима, так как может не иметь смысла для некоторых типов данных. Например, протокол `Size` имеет смысл для перечислимых типов данных, но не для чисел. Поэтому зачастую выброс ошибки при отсутствии необходимой реализации протокола является ожидаемым поведением. - - Какой выбрать из вариантов между явным выведением (derive) или опцией `fallback_to_any` зависит от ситуации. Тем не менее явное лучше, чем неявное, поэтому зачастую в Elixir библиотеках используется подход с `derive`. - -instructions: | - - Продолжим упражнение из прошлого модуля, теперь опишите структуру `Robot` с явным указанием протокола `Teller` и реализуйте протокол для `Any` который возвращает строку `World!`: - - ```elixir - Teller.say_something(%Human{name: "John"}) # => "Hello, world!" - Teller.say_something(%Dog{name: "Barkinson"}) # => "Bark, world!" - Teller.say_something(%Cat{name: "Meowington"}) # => "Meow, world!" - Teller.say_something(%Robot{name: "Roberto"}) # => "World!" - ``` - - Реализация для `Any` должна быть описана раньше, чем структуры. - -tips: - - | - [Официальная документация](https://hexdocs.pm/elixir/Protocol.html#derive/3) diff --git a/modules/45-structs/50-any-protocols/en/data.yml b/modules/45-structs/50-any-protocols/en/data.yml index 1e22bd8..d95bb44 100644 --- a/modules/45-structs/50-any-protocols/en/data.yml +++ b/modules/45-structs/50-any-protocols/en/data.yml @@ -1,2 +1,3 @@ +--- name: Any protocols tips: [] diff --git a/modules/45-structs/50-any-protocols/ru/data.yml b/modules/45-structs/50-any-protocols/ru/data.yml index 31a93c3..a5f2c25 100644 --- a/modules/45-structs/50-any-protocols/ru/data.yml +++ b/modules/45-structs/50-any-protocols/ru/data.yml @@ -1,3 +1,4 @@ +--- name: Общие протоколы tips: - | diff --git a/modules/50-errors/10-errors-handling/description.en.yml b/modules/50-errors/10-errors-handling/description.en.yml deleted file mode 100644 index 236000a..0000000 --- a/modules/50-errors/10-errors-handling/description.en.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- -name: Errors handling, exceptions -theory: | - - 🚧 In development - -instructions: | - - 🚧 In development - -tips: [] diff --git a/modules/50-errors/10-errors-handling/description.ru.yml b/modules/50-errors/10-errors-handling/description.ru.yml deleted file mode 100644 index 270c7e0..0000000 --- a/modules/50-errors/10-errors-handling/description.ru.yml +++ /dev/null @@ -1,137 +0,0 @@ ---- -name: Обработка ошибок, исключения -theory: | - - В Elixir есть механизм исключений, однако используется он иначе, чем в классических языках программирования. Для начала ознакомимся с исключениями. - - Исключения выбрасываются через `raise`: - - ```elixir - raise "Boom!" - # => ** (RuntimeError) Boom! - # => iex:149: (file) - ``` - - Исключения создаются через `defexception`: - - ```elixir - defmodule MyError do - defexception message: "boom!" - end - - raise MyError - # => ** (MyError) boom! - # => iex:150: (file) - - raise MyError, message: "different boom!" - # => ** (MyError) different boom! - # => iex:150: (file) - ``` - - Исключения перехватываются через `try` и `rescue`: - - ```elixir - try do - raise "boom!" - rescue - e in RuntimeError -> e - end - # => %RuntimeError{message: "boom!"} - - try do - raise "boom!" - rescue - RuntimeError -> "oops" - end - # => "oops" - ``` - - Как уже было сказано, в мире Elixir используется философия "пусть сломается" (let it crash), из-за чего исключения при написании Elixir кода зачастую не обрабатываются. Почему? Из-за того, что Elixir код всегда исполняется в отдельных процессах (акторах), которые работают независимо. Поэтому падение одного из сотни или тысячи процессов никак не повлияет на работу всей системы. Про процессы (акторы) подробнее поговорим в следующем модуле, а пока продолжим изучать исключения. - - Так как Elixir программисты зачастую игнорируют прямую обработку исключений, иногда бывает полезно поймать исключение, записать в логи информацию об ошибке и пробросить исключение дальше по стеку. Повторный выброс исключения происходит через `reraise`: - - ```elixir - require Logger - - try do - 1 / 0 - rescue - e -> - Logger.warning(Exception.format(:error, e, __STACKTRACE__)) - reraise e, __STACKTRACE__ - end - - # => ** (ArithmeticError) bad argument in arithmetic expression: 1 / 0 - # => :erlang./(1, 0) - # => iex:157: (file) - # => iex:161: (file) - - # => 17:48:09.214 [warning] ** (ArithmeticError) bad argument in arithmetic expression - # => :erlang./(1, 0) - # => iex:157: (file) - # => (elixir 1.15.0) src/elixir.erl:374: anonymous fn/4 in :elixir.eval_external_handler/1 - # => (stdlib 4.3.1.1) erl_eval.erl:748: :erl_eval.do_apply/7 - # => (stdlib 4.3.1.1) erl_eval.erl:987: :erl_eval.try_clauses/10 - # => (elixir 1.15.0) src/elixir.erl:359: :elixir.eval_forms/4 - # => (elixir 1.15.0) lib/module/parallel_checker.ex:112: Module.ParallelChecker.verify/1 - # => (iex 1.15.0) lib/iex/evaluator.ex:331: IEx.Evaluator.eval_and_inspect/3 - ``` - - По сути, исключения в Elixir используются по прямому назначению, они выбрасываются только когда произошло действительно что-то, чего в нормальной ситуации произойти не должно. В большинстве же остальных языков программирования исключения используются как еще один способ управлять потоком выполнения программы. Почти оператор `GOTO`, только замаскированный. - - Иногда нужно подчистить какой-то ресурс, при возникновении исключения, тогда подойдет `after`: - - ```elixir - {:ok, file} = File.open("sample", [:utf8, :write]) - - try do - IO.write(file, "hello") - raise "oops" - after - File.close(file) - end - ``` - - Если же исключения не возникло, но нужно выполнить еще какой-то код, тогда подойдет `else`: - - ```elixir - x = 2 - - try do - 1 / x - rescue - ArithmeticError -> - :infinity - else - y when y < 1 and y > -1 -> :small - _ -> :large - end - # => :small - ``` - -instructions: | - - Создайте функцию `my_div`, которая выбрасывает исключение `ArgumentError` с сообщением `Divide x by zero is prohibited!`, где `x` - первый переданный аргумент. Для деления с округлением воспользуйтесь `Integer.floor_div`: - - ```elixir - Solution.my_div(128, 2) - # => 64 - - Solution.my_div(128, 0) - # => ** (ArgumentError) Divide 128 by zero is prohibited! - # => iex:142: Solution.my_div/2 - # => iex:149: (file) - - Solution.my_div(10, 0) - # => ** (ArgumentError) Divide 10 by zero is prohibited! - # => iex:142: Solution.my_div/2 - # => iex:149: (file) - ``` - -tips: - - | - [Официальная документация](https://hexdocs.pm/elixir/Exception.html) - - | - [Статья про исключения](https://belaycpp.com/2021/06/16/exceptions-are-just-fancy-gotos/) - - | - [Исключения на примере JS](https://ru.hexlet.io/courses/js_errors/lessons/exceptions/theory_unit) diff --git a/modules/50-errors/10-errors-handling/en/data.yml b/modules/50-errors/10-errors-handling/en/data.yml index 3736fc7..13ff0e8 100644 --- a/modules/50-errors/10-errors-handling/en/data.yml +++ b/modules/50-errors/10-errors-handling/en/data.yml @@ -1,2 +1,3 @@ +--- name: Errors handling, exceptions tips: [] diff --git a/modules/50-errors/10-errors-handling/ru/data.yml b/modules/50-errors/10-errors-handling/ru/data.yml index 174e6fe..33f87b0 100644 --- a/modules/50-errors/10-errors-handling/ru/data.yml +++ b/modules/50-errors/10-errors-handling/ru/data.yml @@ -1,3 +1,4 @@ +--- name: Обработка ошибок, исключения tips: - | diff --git a/modules/50-errors/20-tuple-errors-processing/description.en.yml b/modules/50-errors/20-tuple-errors-processing/description.en.yml deleted file mode 100644 index 2de1801..0000000 --- a/modules/50-errors/20-tuple-errors-processing/description.en.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- -name: Errors handling using tuples -theory: | - - 🚧 In development - -instructions: | - - 🚧 In development - -tips: [] diff --git a/modules/50-errors/20-tuple-errors-processing/description.ru.yml b/modules/50-errors/20-tuple-errors-processing/description.ru.yml deleted file mode 100644 index 6cf924d..0000000 --- a/modules/50-errors/20-tuple-errors-processing/description.ru.yml +++ /dev/null @@ -1,72 +0,0 @@ ---- - -name: Обработка ошибок с помощью кортежей -theory: | - - Как было описано в прошлом упражнении, в Elixir не используют исключения напрямую для обработки ошибок и управлением работы программы. Вместо этого из функций возвращают кортежи, если операция была успешна, то `{:ok, result}`, где `result` - результат выполнения функции. В случае ошибки возвращается кортеж вида `{:error, reason}` - где `reason` причина ошибки, может быть любого формата. Рассмотрим примеры: - - ```elixir - File.copy("/not_existing_dir", "/existing_dir") - # => {:error, :enoent} - File.copy("/some_dir", "/existing_dir") - # => {:ok, 210} - ``` - - Важно отметить, что для булевых функций, лучше возвращать не кортеж, а `true` или `false`. - - При таком подходе к обработке ошибок, органично использовать паттерн-матчинг: - - ```elixir - defmodule Example do - @magic_number 10 - - def multiply_by_two(number) when is_integer(number) do - {:ok, number * 2} - end - - def multiply_by_two(number) do - {:error, :not_number} - end - - def magic(number) do - case multiply_by_two(number) do - {:ok, result} -> {:ok, result + @magic_number} - {:error, reason} -> {:error, :no_magic_here} - end - end - end - - Example.multiply_by_two(2) - # => {:ok, 4} - Example.multiply_by_two("string") - # => {:error, :not_number} - - Example.magic(2) - # => {:ok, 4} - Example.magic("string") - # => {:error, :no_magic_here} - ``` - - Для простых функций не обязательно возвращать кортеж `{:ok, result}`, достаточно вернуть только `result`. Однако если функция в разных условиях возвращает разный результат, например, словарь с разным набором ключей, тогда лучше использовать кортежи и точнее паттерн-матчить их. - -instructions: | - - Реализуйте функцию `compare`, которая сравнивает два переданных *числа*: - - ```elixir - Solution.compare(2, 3) - # => {:ok, :less} - Solution.compare(3, 3) - # => {:ok, :equal} - Solution.compare(4, 3) - # => {:ok, :greater} - - Solution.compare("", 3) - # => {:error, :not_number} - Solution.compare(2, []) - # => {:error, :not_number} - ``` - -tips: - - | - [Пример модуля с описанным подходом к обработке ошибок](https://hexdocs.pm/elixir/File.html) diff --git a/modules/50-errors/20-tuple-errors-processing/en/data.yml b/modules/50-errors/20-tuple-errors-processing/en/data.yml index 95b0b87..90021bb 100644 --- a/modules/50-errors/20-tuple-errors-processing/en/data.yml +++ b/modules/50-errors/20-tuple-errors-processing/en/data.yml @@ -1,2 +1,3 @@ +--- name: Errors handling using tuples tips: [] diff --git a/modules/50-errors/20-tuple-errors-processing/ru/data.yml b/modules/50-errors/20-tuple-errors-processing/ru/data.yml index 1679a05..5c3f0b0 100644 --- a/modules/50-errors/20-tuple-errors-processing/ru/data.yml +++ b/modules/50-errors/20-tuple-errors-processing/ru/data.yml @@ -1,3 +1,4 @@ +--- name: Обработка ошибок с помощью кортежей tips: - > diff --git a/modules/50-errors/30-with-operator/description.en.yml b/modules/50-errors/30-with-operator/description.en.yml deleted file mode 100644 index 38418c2..0000000 --- a/modules/50-errors/30-with-operator/description.en.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- -name: With operator -theory: | - - 🚧 In development - -instructions: | - - 🚧 In development - -tips: [] diff --git a/modules/50-errors/30-with-operator/description.ru.yml b/modules/50-errors/30-with-operator/description.ru.yml deleted file mode 100644 index df97368..0000000 --- a/modules/50-errors/30-with-operator/description.ru.yml +++ /dev/null @@ -1,147 +0,0 @@ ---- - -name: Оператор with -theory: | - - Рассмотрим оператор `with`, зачем нужен и как используется. Для начала, опишем пример: - - ```elixir - defmodule Example do - require Integer - - def inc_even(num) do - if Integer.is_even(num) do - num + 1 - else - {:error, :not_even} - end - end - - def stringify_odd(num) do - if Integer.is_odd(num) do - Integer.to_string(num) - else - {:error, :not_odd} - end - end - end - ``` - - Проблема в том, что при попытке скомпоновать эти функции через оператор пайплайна `|>` будут возникать ошибки, если аргумент окажется неподходящим: - - ```elixir - 2 |> Example.inc_even() |> Example.stringify_odd() - # => "3" - - 2 |> Example.stringify_odd() |> Example.inc_even() - # => {:error, :not_even} - ``` - - Можно попытаться решить проблему через `case`: - - ```elixir - case Example.inc_even(3) do - {:error, reason} -> reason - - result -> - case Example.stringify_odd(result) do - {:error, reason} -> reason - - stringified -> stringified - end - end - - # => :not_even - ``` - - Однако, такой подход начинает хуже работать с увеличением вложенности операций. Для *выпрямления* таких вычислений используется конструкция `with`, с помощью которой описывается удачный путь вычислений (happy path) и комбинировать вызовы функций с разным форматом данных. Перепишем на `with`: - - ```elixir - with incremented <- Example.inc_even(2), - stringified <- Example.stringify_odd(incremented), - do: stringified - - # => "3" - # по сути получился аналог - # 2 |> Example.inc_even() |> Example.stringify_odd() - ``` - - Оператор `with` полезен тем, что можно обрабатывать неудачные вызовы функций в ветке `else`: - - ```elixir - with incremented <- Example.inc_even(3), - stringified <- Example.stringify_odd(incremented) do - stringified - else - {:error, reason} -> reason - end - - # => :not_odd - ``` - - Внутри `with` можно использовать разные функции с разными возвращаемыми значениями, например: - - ```elixir - user = %{name: "John"} - with updated_user <- Map.put(user, :age, 20), - true <- updated_user[:age] == 20, - %{hobby: hobby} <- Map.put(updated_user, :hobby, "diving"), - do: hobby - - # => "diving" - ``` - - Для функции предиката добавим еще одну ветку и поменяем значение по ключу `age`: - - ```elixir - user = %{name: "John"} - with updated_user <- Map.put(user, :age, 22), - true <- updated_user[:age] == 20, - %{hobby: hobby} <- Map.put(updated_user, :hobby, "diving") do - hobby - else - false -> "incorrect age" - end - - # => "incorrect age" - ``` - - С помощью `with` проще структурировать ошибки, которые могут возникнуть в цепочке вычислений, однако важно не увлечься при описании такой цепочки, потому что с ростом операций становится тяжелее понять что происходит с данными. В таких случаях лучше сгруппировать часть операции в отдельные функции и в итоговом `with` описать меньшее количество вызываемых функций. Хорошим примером является фреймворк Phoenix, в котором при генерации ресурса, например пользователя, создается такой код для контроллера: - - ```elixir - def create(conn, params) do - with {:ok, user} <- Users.create_user(params) do - conn - |> put_status(:created) - |> render("show.json", user: user) - end - ``` - - На верхнем уровне Phoenix перехватывает ошибки создания ресурса, так как оно типично: `{:error, changeset}`. Поэтому нет необходимости описывать отдельно ветку `else`. - -instructions: | - - Реализуйте функцию `validate`, которая проверяет переданный аргумент на следующие условия: - - аргумент является строкой - - длина строки меньше или равна 8 - - длина строки больше или равна 2 - - Примеры работы функции: - - ```elixir - Solution.validate("some") - # => {:ok, "some"} - Solution.validate("hello!!") - # => {:ok, "hello!!"} - - Solution.validate(1) - # => {:error, :not_binary} - Solution.validate("a") - # => {:error, :too_short} - Solution.validate("hello, world!") - # => {:error, :too_long} - ``` - -tips: - - | - [Официальная документация](https://hexdocs.pm/elixir/Kernel.SpecialForms.html#with/1) diff --git a/modules/50-errors/30-with-operator/en/data.yml b/modules/50-errors/30-with-operator/en/data.yml index 3d4437e..652a3cf 100644 --- a/modules/50-errors/30-with-operator/en/data.yml +++ b/modules/50-errors/30-with-operator/en/data.yml @@ -1,2 +1,3 @@ +--- name: With operator tips: [] diff --git a/modules/50-errors/30-with-operator/ru/data.yml b/modules/50-errors/30-with-operator/ru/data.yml index c1ede63..03de120 100644 --- a/modules/50-errors/30-with-operator/ru/data.yml +++ b/modules/50-errors/30-with-operator/ru/data.yml @@ -1,3 +1,4 @@ +--- name: Оператор with tips: - > diff --git a/modules/55-processes/10-processes-intro/description.en.yml b/modules/55-processes/10-processes-intro/description.en.yml deleted file mode 100644 index fceb11b..0000000 --- a/modules/55-processes/10-processes-intro/description.en.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- -name: Intoduction to processes -theory: | - - 🚧 In development - -instructions: | - - 🚧 In development - -tips: [] diff --git a/modules/55-processes/10-processes-intro/description.ru.yml b/modules/55-processes/10-processes-intro/description.ru.yml deleted file mode 100644 index 9d038fd..0000000 --- a/modules/55-processes/10-processes-intro/description.ru.yml +++ /dev/null @@ -1,69 +0,0 @@ ---- -name: Знакомство с процессами -theory: | - - Весь Elixir код запускается в рамках *процесса*. Важно понимать, что процесс в Elixir не является аналогией процесса операционной системы. - - Процессы в Elixir являются зелеными потоками (green threads) - это более легковесные процессы, чем в операционной системе, которые выполняются в рамках виртуальной машины языка программирования. В случае Elixir это BEAM. Внутри виртуальной машины выделяются отдельные планировщики, как и в операционной системе, которые следят за тем, чтобы каждый процесс работал определенный квант времени, например 4 миллисекунды. Благодаря такой отлаженной работы планировщиков нагрузка на процессор распределяется более плавно относительно каждого из ядер, то есть ситуации, когда одно ядро нагружено сильнее другого возникают намного реже. По сути, BEAM реализует на практике вытесняющую многозадачность. - - Процессы изолированы и выполняются *конкурентно* относительно друг друга. Общаются процессы между собой через *сообщения*, забавный факт, что первая предложенная Аланом Кеем модель ООП как раз описывалась как множество объектов с внутренним состоянием, которые коммуницируют между собой через сообщения. - - Из-за легковесности процессов, зачастую в Elixir приложениях запущенны сотни, а то и тысячи процессов. Для создания процесса используем функцию `spawn`: - - ```elixir - spawn(fn -> 2 * 2 end) - # => #PID<0.43.0> - - # создадим 100 тысяч процессов - processes = 0..100_000 |> Enum.map(fn _ -> spawn(fn -> 2 + 2 end) end) - # => много иднтификаторов процессов - - hd(processes) - # => #PID<0.5249.9> - List.last(processes) - # => #PID<0.6945.12> - ``` - - Функция `spawn` принимает аргументом функцию, которую запускает в отдельном процессе. Заметьте, что вызов `spawn` возвращает не результат выполнения переданной функции, а идентификатор процесса (process identifier) или ПИД (PID). Процесс, который был создан в предыдущем примере, после выполнения функции завершился. - - ```elixir - pid = spawn(fn -> 2 * 2 end) - # => #PID<0.44.0> - - Process.alive?(pid) - # => false - ``` - - С помощью функции `self` мы можем узнать идентификатор нынешнего процесса, в котором была вызвана функция: - - ```elixir - self() - # => #PID<0.41.0> - - Process.alive?(self()) - # => true - ``` - -instructions: | - - Создайте функцию `run_in_process`, которая принимает функцию и создает процесс, который выполняет эту функцию: - - ```elixir - Solution.run_in_process(fn -> String.upcase("code basics") end) - # => #PID<0.42.0> - - Solution.run_in_process(fn -> Process.sleep(1000) end) - # => #PID<0.43.0> - ``` - -tips: - - | - [Про green threads](https://ru.wikipedia.org/wiki/Green_threads) - - | - [Про ООП в Erlang](https://www.infoq.com/interviews/johnson-armstrong-oop/) - - | - [Видео про BEAM и процессы](https://www.youtube.com/watch?v=JvBT4XBdoUE) - - | - [Официальная документация](https://hexdocs.pm/elixir/Process.html) - - | - [Про вытесняющую многозадачность](https://ru.wikipedia.org/wiki/%D0%92%D1%8B%D1%82%D0%B5%D1%81%D0%BD%D1%8F%D1%8E%D1%89%D0%B0%D1%8F_%D0%BC%D0%BD%D0%BE%D0%B3%D0%BE%D0%B7%D0%B0%D0%B4%D0%B0%D1%87%D0%BD%D0%BE%D1%81%D1%82%D1%8C) diff --git a/modules/55-processes/10-processes-intro/en/data.yml b/modules/55-processes/10-processes-intro/en/data.yml index 03ed723..01e2e2e 100644 --- a/modules/55-processes/10-processes-intro/en/data.yml +++ b/modules/55-processes/10-processes-intro/en/data.yml @@ -1,2 +1,3 @@ +--- name: Intoduction to processes tips: [] diff --git a/modules/55-processes/10-processes-intro/ru/data.yml b/modules/55-processes/10-processes-intro/ru/data.yml index fb9c680..a19d542 100644 --- a/modules/55-processes/10-processes-intro/ru/data.yml +++ b/modules/55-processes/10-processes-intro/ru/data.yml @@ -1,3 +1,4 @@ +--- name: Знакомство с процессами tips: - | diff --git a/modules/55-processes/20-processes-mailbox/description.en.yml b/modules/55-processes/20-processes-mailbox/description.en.yml deleted file mode 100644 index f8188ee..0000000 --- a/modules/55-processes/20-processes-mailbox/description.en.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- -name: Processes mailbox -theory: | - - 🚧 In development - -instructions: | - - 🚧 In development - -tips: [] diff --git a/modules/55-processes/20-processes-mailbox/description.ru.yml b/modules/55-processes/20-processes-mailbox/description.ru.yml deleted file mode 100644 index 1e7cc84..0000000 --- a/modules/55-processes/20-processes-mailbox/description.ru.yml +++ /dev/null @@ -1,130 +0,0 @@ ---- -name: Почтовый ящик процессов -theory: | - - Как было упомянуто в прошлом занятии, процессы общаются между собой с помощью *сообщений*. Сообщения всегда отправляются *асинхронно*, без подтверждения доставки сообщения процессу. Чтобы получить результат обработки сообщения другим процессом, используется блокировка в ожидании ответного сообщения от другого процесса, но это уже тонкости реализации синхронного вызова. По сути способ синхронизации результата это низкоуровневые детали, с которыми сталкиваться скорее всего не придется. Для отправки сообщений используется функция `send`. Каждый процесс имеет собственный *почтовый ящик*, который он читает с помощью функции `receive`. Рассмотрим на примере: - - ```elixir - send(self(), {:hello, "world"}) - # => {:hello, "world"} - - receive do - {:hello, msg} -> msg - {:world, _} -> "unmatched expression" - end - # => "world" - ``` - - Блок кода `receive` при получении сообщения, проходится по всему почтовому ящику в поиске совпадений по шаблону сообщений. - - Отправка сообщений через `send` является неблокирующей операцией, то есть сообщения процессу отправляются *асинхронно*. Так как весь Elixir код выполняется в каком-то процессе, то процесс может отправлять сообщения самому себе. - - Почтовые ящики у процессов могут переполняться, поэтому важно указывать базовый паттерн, в который попадут все остальные сообщения из ящика. Блок `receive` закончит выполнение при первом совпадении паттерна, в ином случае процесс заблокируется в ожидании подходящего сообщения, либо, по истечению указанного таймаута: - - ```elixir - receive do - {:hello, msg} -> msg - after - 1_000 -> "message await stopped after 1 second" - end - # => "message await stopped after 1 second" - ``` - - Теперь создадим отдельный процесс, который отправит сообщение в процесс родитель: - - ```elixir - parent = self() - # => #PID<0.105.0> - - spawn(fn -> send(parent, {:hello, self()}) end) - # => #PID<0.6946.12> - - receive do - {:hello, pid} -> "Got hello from #{inspect(pid)}" - end - # => "Got hello from #PID<0.6946.12>" - ``` - - Так как `receive` завершается по таймауту или успешной обработке сообщения, то как сделать обработку процессом сообщений постоянной? Рекурсивный вызов `receive`, конечно же! - - ```elixir - defmodule Processor do - def process() do - receive do - {:hello, msg} -> - IO.puts(msg) - process() - - {:world, msg} -> - IO.puts(String.upcase(msg)) - process() - - {:exit, _} -> IO.puts("Shutdown processor...") - end - end - end - - processor = spawn(fn -> Processor.process() end) - # => #PID<0.6952.12> - Process.alive?(processor) - # => true - - send(processor, {:hello, "code basic"}) - # для просмотра почтового ящика родительского процесса - # далее по коду будет подразумеваться, что эта строчка кода - # будет вызываться после каждой отправки сообщения другому процессу - Process.info(parent, :messages) - # => code basic - - Process.alive?(processor) - # => true - - send(processor, {:world, "code basic"}) - # => CODE BASIC - Process.alive?(processor) - # => true - - send(processor, {:exit, ""}) - # => Shutdown processor... - Process.alive?(processor) - # => false - ``` - -instructions: | - - Создайте функцию `calculate` которая принимает процесс-получатель и поддерживает операции `:+`, `:-`, `:*` в виде сообщений от процесса и при обработке возвращает сообщение процессу отправителю с результатом, функция должна поддерживать постоянную обработку сообщений. При передаче сообщения `:exit`, функция перестает обрабатывать входящие сообщения: - - ```elixir - parent = self() - calculator = spawn(fn -> Solution.calculate(parent) end) - - send(calculator, {:+, [2, 5]}) - receive do - {:ok, result} -> result - end - # => 7 - Process.alive?(calculator) - # => true - - send(calculator, {:*, [2, 5]}) - receive do - {:ok, result} -> result - end - # => 10 - Process.alive?(calculator) - # => true - - send(calculator, {:exit}) - receive do - {:ok, result} -> result - end - # => :exited - Process.alive?(calculator) - # => false - ``` - -tips: - - | - [Конкурентность на примере процессов Elixir](https://www.youtube.com/watch?v=A4x6IfceJCM) - - | - [Официальная документация](https://hexdocs.pm/elixir/Process.html) diff --git a/modules/55-processes/20-processes-mailbox/en/data.yml b/modules/55-processes/20-processes-mailbox/en/data.yml index 970a14e..e4fa495 100644 --- a/modules/55-processes/20-processes-mailbox/en/data.yml +++ b/modules/55-processes/20-processes-mailbox/en/data.yml @@ -1,2 +1,3 @@ +--- name: Processes mailbox tips: [] diff --git a/modules/55-processes/20-processes-mailbox/ru/data.yml b/modules/55-processes/20-processes-mailbox/ru/data.yml index a44418c..fd18170 100644 --- a/modules/55-processes/20-processes-mailbox/ru/data.yml +++ b/modules/55-processes/20-processes-mailbox/ru/data.yml @@ -1,3 +1,4 @@ +--- name: Почтовый ящик процессов tips: - > diff --git a/modules/55-processes/30-processes-state/description.en.yml b/modules/55-processes/30-processes-state/description.en.yml deleted file mode 100644 index f35b2d2..0000000 --- a/modules/55-processes/30-processes-state/description.en.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- -name: Processes state -theory: | - - 🚧 In development - -instructions: | - - 🚧 In development - -tips: [] diff --git a/modules/55-processes/30-processes-state/description.ru.yml b/modules/55-processes/30-processes-state/description.ru.yml deleted file mode 100644 index 9b79456..0000000 --- a/modules/55-processes/30-processes-state/description.ru.yml +++ /dev/null @@ -1,141 +0,0 @@ ---- -name: Состояние процесса -theory: | - - В прошлом упражнении мы создали процесс-калькулятор, который только обрабатывает входящие запросы, не храня никакого *состояния*. Но как его сохранить? Основная идея процесса, который обрабатывает более одного сообщения за раз, это вызов рекурсивной функции, которая продолжит основной цикл обработки сообщений. Рассмотрим пример с процессом-счетчиком: - - ```elixir - defmodule Counter do - def init(parent, initial_state \\ 0) do - process(parent, initial_state) - end - - defp process(parent, state) do - receive do - :inc -> - new_state = state + 1 - send(parent, {:ok, new_state}) - process(parent, new_state) - - :dec -> - new_state = state - 1 - send(parent, {:ok, new_state}) - process(parent, new_state) - - :current -> - send(parent, {:ok, state}) - process(parent, state) - - :reset -> - send(parent, {:ok, 0}) - process(parent, 0) - end - end - end - ``` - - В процессе-счетчике мы вынесли функцию запуска процесса, запомнили процесс-родитель и инициализировали состояние процесса-счетчика нулем. Затем при обработке сообщения изменяется состояние процесса, которое передается дальше в рекурсивный вызов функции `process`. Теперь запустим счетчик и проверим его работу: - - Важно, что идентификатор родительского процесса мы передаем снаружи, так как вызов `self()` внутри модуля вернет иной идентификатор. - - ```elixir - parent = self() - counter = spawn(fn -> Counter.init(parent) end) - - send(counter, :current) - # для просмотра почтового ящика родительского процесса - # далее по коду будет подразумеваться, что эта строчка кода - # будет вызываться после каждой отправки сообщения другому процессу - Process.info(parent, :messages) - # => {:messages, [ok: 0]} - - send(counter, :inc) - # => {:ok, 1} - send(counter, :inc) - # => {:ok, 2} - - send(counter, :dec) - # => {:ok, 1} - - send(counter, :inc) - # => {:ok, 2} - - send(counter, :reset) - # => {:ok, 0} - - send(counter, :dec) - # => {:ok, -1} - ``` - - Важно заметить, что каждый процесс хранит свое состояние *отдельно* от других процессов: - - ```elixir - parent = self() - first_counter = spawn(fn -> Counter.init(parent) end) - second_counter = spawn(fn -> Counter.init(parent) end) - - send(first_counter, :inc) - send(first_counter, :inc) - send(first_counter, :inc) - - send(second_counter, :dec) - send(second_counter, :dec) - - send(first_counter, :current) - # => {:ok, 3} - - send(second_counter, :current) - # => {:ok, -2} - ``` - - Состояние не обязательно выражается числом, в него можно складывать что-угодно, начиная от чисел и заканчивая словарем со ссылками на другие процессы, к примеру. - -instructions: | - - В этот раз создайте процесс кеш-сервер, который хранит в состоянии словарь. Напишите функцию `init`, которая принимает родительский процесс и начальное состояние (как в примерах выше). Затем добавьте обработку следующих сообщений: - - `:put` положить по переданному ключу `key` значение `value` в состояние кеш-сервера, возвращает кортеж `{:ok, value}`; - - `:get` достать из состояния процесса значение по переданному ключу, при отсутствии значения вернуть кортеж `{:error, :not_found}`; - - `:drop` убрать из состояния процесса переданный ключ; - - `:exit` перестать обрабатывать входящие сообщения кеш-сервером, возвращает кортеж `{:ok, :exited}`; - - а еще добавьте обработку невалидных сообщений, чтобы процесс не ломался, если ему передать что-то не то (вернуть кортеж `{:error, :unrecognized_operation}`). - - ```elixir - parent = self() - cache_server = spawn(fn -> CacheServer.init(parent) end) - - send(cache_server, {:put, {:hello, "world"}}) - # => {:ok, "world"} - - send(cache_server, {:get, {:hello}}) - # => {:ok, "world"} - - send(cache_server, {:get, {:some}}) - # => {:error, :not_found}) - - send(cache_server, {:drop, {:hello}}) - # => {:ok, :hello} - - send(cache_server, {:get, {:hello}}) - # => {:error, :not_found}) - - send(cache_server, {:something, {1, 2, 3}}) - # => {:error, :unrecognized_operation} - Process.alive?(cache_server) - # => true - - send(cache_server, :boom) - # => {:error, :unrecognized_operation} - Process.alive?(cache_server) - # => true - - send(cache_server, {:exit}) - # => {:ok, :exited} - Process.alive?(cache_server) - # => false - ``` - -tips: - - | - [Конкурентность на примере процессов Elixir](https://www.youtube.com/watch?v=A4x6IfceJCM) - - | - [Официальная документация](https://hexdocs.pm/elixir/Process.html) diff --git a/modules/55-processes/30-processes-state/en/data.yml b/modules/55-processes/30-processes-state/en/data.yml index da570e9..7f9566d 100644 --- a/modules/55-processes/30-processes-state/en/data.yml +++ b/modules/55-processes/30-processes-state/en/data.yml @@ -1,2 +1,3 @@ +--- name: Processes state tips: [] diff --git a/modules/55-processes/30-processes-state/ru/data.yml b/modules/55-processes/30-processes-state/ru/data.yml index ca2f05f..ceac150 100644 --- a/modules/55-processes/30-processes-state/ru/data.yml +++ b/modules/55-processes/30-processes-state/ru/data.yml @@ -1,3 +1,4 @@ +--- name: Состояние процесса tips: - > diff --git a/modules/55-processes/35-links/description.en.yml b/modules/55-processes/35-links/description.en.yml deleted file mode 100644 index 6253022..0000000 --- a/modules/55-processes/35-links/description.en.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- -name: Process links -theory: | - - 🚧 In development - -instructions: | - - 🚧 In development - -tips: [] diff --git a/modules/55-processes/35-links/description.ru.yml b/modules/55-processes/35-links/description.ru.yml deleted file mode 100644 index 403422a..0000000 --- a/modules/55-processes/35-links/description.ru.yml +++ /dev/null @@ -1,158 +0,0 @@ ---- -name: Связь процессов -theory: | - - Так как процессы изолированы друг от друга, то возникновение ошибки в одном процессе никак не отражается на работе остальных: - - ```elixir - spawn(fn -> raise "boom" end) - # => 17:27:46.980 [error] Process #PID<0.277.0> raised an exception - # => ** (RuntimeError) boom - # => iex:156: (file) - ``` - - Однако, в некоторых ситуациях нужно, чтобы процессы были связаны между собой для обработки ошибок. Создание связи происходит через функцию `spawn_link`: - - ```elixir - self() - # => #PID<0.105.0> - - spawn_link(fn -> raise "boom" end) - # => ** (EXIT from #PID<0.105.0>) shell process exited with reason: an exception was raised: - # => ** (RuntimeError) boom - # => iex:158: (file) - - # => 17:30:45.109 [error] Process #PID<0.280.0> raised an exception - # => ** (RuntimeError) boom - # => iex:158: (file) - ``` - - Теперь, когда процессы связаны, возникновение ошибки в созданном процессе так же вызвало ошибку и в процессе, с которым он был связан. По сути, связанный процесс получил сигнал `EXIT` и аварийно завершился. - - Благодаря связям, можно создавать процессы *супервизоры* (supervisors), которые следят за порожденными процессами *рабочими* (workers). При возникновении внештатных ситуаций процесс супервизор перезапускает наблюдаемый им процесс. Такая архитектура лежит в основе Elixir и Erlang приложений и называется *дерево супервизии*, внутри которого отдельные процессы супервизоры запускают другие процессы и следят за их работой, перезапуская при необходимости. Корнем в этом дереве выступает само приложение, внутри которого запущены остальные процессы. Выглядит дерево супервизии примерно так: - - ```elixir - # супервизор игровой сессии -- обработчики игровых сессий - # / - # процесс приложения -- супервизор обработки платежей -- обработчики платежей - # \ - # супервизор рассылки уведомлений -- отправители уведомлений - ``` - - Теперь рассмотрим причины завершения процессов. Когда процесс заканчивает свою работу, он *завершается* (finishes) в *нормальном* (normal) режиме, по сути нормальное завершение процесса выглядит как прямой вызов функции `exit(:normal)` из модуля `Process`: - - ```elixir - spawn_link(fn -> exit(:normal) end) - # => #PID<0.282.0> - spawn(fn -> exit(:normal) end) - # => #PID<0.283.0> - ``` - - Результат работы функций идентичный из-за завершения процесса в режиме `:normal`. Если же причина будет другой, то завершение процесса будет расцениваться как *аварийное*: - - ```elixir - spawn(fn -> exit(:boom_reason) end) - # => #PID<0.284.0> - - spawn_link(fn -> exit(:boom_reason) end) - # => ** (EXIT from #PID<0.281.0>) shell process exited with reason: :boom_reason - # - # => Interactive Elixir (1.15.0) - press Ctrl+C to exit (type h() ENTER for help) - ``` - - Так как процесс интерактивной оболочки был связан напрямую с новым процессом, который аварийно завершился, то процесс интерактивной оболочки тоже аварийно завершился. А дальше *супервизор интерактивной оболочки* перехватил аварийное завершение *процесса-оболочки* и перезапустил его. - - Именно так и работает дерево супервизии. Но для перезапуска нужно как-то понять, каким образом завершился наблюдаемый процесс. Для этих целей используется перехват выхода или перехват завершения процесса (trap exits) через специальный флаг, который выставляется для настройки процесса. Процессы можно конфигурировать, например размер кучи, приоритет, перехват завершения и многое другое. Нас интересует флаг перехвата, который конвертирует все сигналы завершения процесса в сообщения вида `{'EXIT', from, reason}`. Настройка процессов таким образом не является стандартной практикой, так как фреймворк OTP предоставляет специальную абстракцию под это, которая и называется `Supervisor`. С OTP фреймворком познакомимся чуть позже, когда разберемся как процессы работают на более низком уровне. - - Теперь настроим процесс на перехват сигналов о завершении: - - ```elixir - Process.flag(:trap_exit, true) - # => false - - spawn_link(fn -> 1 + 1 end) - # => #PID<0.287.0> - Process.info(self(), :messages) - # => {:messages, [{:EXIT, #PID<0.287.0>, :normal}]} - ``` - - Как видно из кода, процесс посчитал выражение и завершился в штатном режиме. Теперь завершим процесс с другим сигналом: - - ```elixir - spawn_link(fn -> exit(:not_okay_reason) end) - # => #PID<0.288.0> - Process.info(self(), :messages) - # =>{:messages, [{:EXIT, #PID<0.288.0>, :not_okay_reason}]} - ``` - - Теперь мы можем перехватывать любые сообщения о завершении и реагировать на них: - - ```elixir - receive do - {:EXIT, pid, :normal} -> "Process #{inspect(pid)} exited normally" - {:EXIT, pid, reason} -> "Process #{inspect(pid)} exited abnormally #{reason}" - end - ``` - - Более того, теперь мы можем перехватывать и исключения: - - ```elixir - spawn_link(fn -> raise "boom!" end) - # => #PID<0.107.0> - # - # => 02:15:35.982 [error] Process #PID<0.107.0> raised an exception - # => ** (RuntimeError) boom! - # => iex:4: (file) - - Process.info(self(), :messages) - # => {:messages, [{:EXIT, #PID<0.107.0>, {%RuntimeError{message: "boom!"}, [{:elixir_eval, :__FILE__, 1, [file: ~c"iex", line: 4]}]}}]} - ``` - -instructions: | - - В файле с решением описан модуль `Worker`. Для работы с ним, нужно создать и связать процесс с этим модулем следующим образом: - - ```elixir - number = 5 - spawn_link(fn -> Worker.work(number) end) - ``` - - После этого, `Worker` проверяет число по правилам классической задачи с собеседований `FooBar` и процесс завершается с соответствующим сигналом: - - число кратно 3 и 5 - `:foobar`; - - число кратно 3 - `:foo`; - - число кратно 3 - `:bar`; - - в ином случае процесс завершается в штатном режиме `:normal`. - - Создайте функцию `supervise_foobar` которая принимает число, вызывает `Worker` и на основе сигнала о завершении формирует строку, где вместо переданного числа подставляется `Foo`, `Bar`, `FooBar` либо ничего, затем число увеличивается на *единицу* и процесс проверки числа и сбора строки идет дальше. - - Если переданное число больше 100 или меньше 1, то продолжать сбор строки не нужно. Не забудьте о `Process.flag(:trap_exit, true)`, так как иначе не получится собрать информацию о сигналах завершения `Worker`. Примеры: - - ```elixir - Solution.supervise_foobar(0) - # => "" - - Solution.supervise_foobar(-10) - # => "" - - Solution.supervise_foobar(11123) - # => "" - - Solution.supervise_foobar(80) - # => "Bar Foo Foo Bar Foo FooBar Foo Bar Foo Foo Bar" - - Solution.supervise_foobar(100) - # => "Bar" - - Solution.supervise_foobar(75) - # => "FooBar Foo Bar Foo Foo Bar Foo FooBar Foo Bar Foo Foo Bar" - ``` - -tips: - - | - [Отказоустойчивость в Elixir](https://www.youtube.com/watch?v=mkGq1WoEvI4) - - | - [Официальная документация](https://hexdocs.pm/elixir/Process.html#exit/2) - - | - [Документация по настройке процессов](https://www.erlang.org/doc/man/erlang.html#process_flag-2) - - | - [Документация по супервизорам](https://hexdocs.pm/elixir/Supervisor.html) diff --git a/modules/55-processes/35-links/en/data.yml b/modules/55-processes/35-links/en/data.yml index f558b15..a6cf0cf 100644 --- a/modules/55-processes/35-links/en/data.yml +++ b/modules/55-processes/35-links/en/data.yml @@ -1,2 +1,3 @@ +--- name: Process links tips: [] diff --git a/modules/55-processes/35-links/ru/data.yml b/modules/55-processes/35-links/ru/data.yml index f1f2531..a288933 100644 --- a/modules/55-processes/35-links/ru/data.yml +++ b/modules/55-processes/35-links/ru/data.yml @@ -1,3 +1,4 @@ +--- name: Связь процессов tips: - | diff --git a/modules/55-processes/40-tasks-and-agents/description.en.yml b/modules/55-processes/40-tasks-and-agents/description.en.yml deleted file mode 100644 index 08b3d90..0000000 --- a/modules/55-processes/40-tasks-and-agents/description.en.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- -name: Tasks and agents -theory: | - - 🚧 In development - -instructions: | - - 🚧 In development - -tips: [] diff --git a/modules/55-processes/40-tasks-and-agents/description.ru.yml b/modules/55-processes/40-tasks-and-agents/description.ru.yml deleted file mode 100644 index dec888c..0000000 --- a/modules/55-processes/40-tasks-and-agents/description.ru.yml +++ /dev/null @@ -1,160 +0,0 @@ ---- -name: Задачи и агенты -theory: | - - Для упрощения работы с процессами, в Elixir есть модули `Task` и `Agent`. Для начала познакомимся с `Task`. - - Преимущества модуля `Task` в отличие от создания процессов напрямую через `spawn` заключаются в улучшенном выводе ошибок, а так же соответствующим API для обработки таких *задач*: - - ```elixir - Task.start(fn -> raise "boom" end) - # => {:ok, #PID<0.8444.0>} - - # => 16:20:15.164 [error] Task #PID<0.8444.0> started from #PID<0.105.0> terminating - # => ** (RuntimeError) boom - # => iex:368: (file) - # => (elixir 1.15.0) src/elixir.erl:374: anonymous fn/4 in :elixir.eval_external_handler/1 - # => Function: #Function<43.3316493/0 in :erl_eval.expr/6> - # => Args: [] - - Task.start_link(fn -> raise "boom" end) - # => {:ok, #PID<0.8446.0>} - # проверим и очистим почтовый ящик процесса - flush() - # => {:EXIT, #PID<0.8446.0>, - # => {%RuntimeError{message: "boom"}, - # => [ - # => {:elixir_eval, :__FILE__, 1, [file: ~c"iex", line: 372]}, - # => {:elixir, :"-eval_external_handler/1-fun-2-", 4, - # => [file: ~c"src/elixir.erl", line: 374, error_info: %{module: Exception}]} - # => ]}} - ``` - - Помимо улучшенного отчета об ошибке в модуле `Task` есть API для асинхронной обработки задач с помощью функций `Task.async()` и `Task.await()`: - - ```elixir - tasks = 0..5 |> Enum.map(fn x -> Task.async(fn -> x * 2 end) end) - # => [ - # => %Task{ - # => mfa: {:erlang, :apply, 2}, - # => owner: #PID<0.105.0>, - # => pid: #PID<0.8447.0>, - # => ref: #Reference<0.73723457.2325282820.199341> - # => }, - # => %Task{ - # => mfa: {:erlang, :apply, 2}, - # => owner: #PID<0.105.0>, - # => pid: #PID<0.8448.0>, - # => ref: #Reference<0.73723457.2325282820.199342> - # => }, - # => ......... - # => ] - - tasks |> Enum.map(fn task -> Task.await(task) end) - # => [0, 2, 4, 6, 8, 10] - ``` - - В модуле есть и [другие функции](https://hexdocs.pm/elixir/Task.html), с которыми потом можно самостоятельно ознакомиться. - - Теперь рассмотрим модуль `Agent` подробнее. Если нужно создать процесс, который хранит состояние и это состояние необходимо передавать другим процессам или состояние используется одним процессом в течении некоторого времени - в этом случае подходит `Agent`. Примеры: - - ```elixir - defmodule Counter do - use Agent - - def start_link(initial_state \\ 0) do - Agent.start_link(fn -> initial_state end, name: __MODULE__) - end - - def current_value do - Agent.get(__MODULE__, fn state -> state end) - end - - def inc do - Agent.update(__MODULE__, fn state -> state + 1 end) - end - - def dec do - Agent.update(__MODULE__, fn state -> state - 1 end) - end - end - - Counter.start_link(2) - # => {:ok, #PID<0.115.0>} - - Counter.current_value() - # => 2 - - Counter.inc() - # => :ok - Counter.current_value() - # => 3 - - Counter.dec() - # => ok - Counter.current_value() - # => 2 - ``` - - Так как агент запущен в отдельном процессе, счетчик может безопасно изменяться при конкурентной обработке. - - В функции `start_link` происходит *глобальная* регистрация процесса-агента и его связывание с процессом из которого вызвана функция, подобно функции `spawn_link`. Про регистрацию процессов будет рассказано в следующих упражнениях. - - В функции `current_value` происходит обращение к внутреннему состоянию процесса и возврат этого состояния наружу. - - В функциях `inc` и `dec` изменяется состояние процесса-агента, увеличиваясь или уменьшаясь на единицу соответственно. - - Модуль `Agent` предполагает разделение на клиентскую и серверную часть. Функции, переданные в качестве аргументов для модуля `Agent` вызываются внутри агента (серверная часть). В ином случае вызов кода считается клиентской частью. Рассмотрим подробнее на примере: - - ```elixir - # вычисление произойдет на стороне процесс-агента, то есть на серверной части - def do_something(agent) do - Agent.get(agent, fn state -> run_slow_code(state) end) - end - - # вычисление произойдет на стороне процесса, вызвавшего функцию, то есть на клиентской части - def do_something(agent) do - Agent.get(agent, fn state -> state end) |> run_slow_code() - end - ``` - - Первая функция блокирует работу агента. Вторая функция копирует все состояние на клиента и затем выполняет операцию на клиенте. При этом следует учитывать, достаточно ли велики данные, чтобы выполнить их обработку на сервере, или их можно быстро переслать клиенту. Другой фактор - нужно ли обрабатывать данные атомарно: получение состояния и вызов run_slow_code(state) вне агента означает, что состояние агента может быть обновлено в это время. Это особенно важно в случае обновлений, поскольку вычисление нового состояния на клиенте, а не на сервере может привести к условиям гонки, если несколько клиентов будут пытаться обновить одно и то же состояние на разные значения. - - В модуле `Agent` есть и [другие функции](https://hexdocs.pm/elixir/Agent.html), которые потом можно самостоятельно изучить. - - После объявления модуля агента, в нем еще появляется функция `child_spec/1`, про ее смысл поговорим дальше, в рамках дерева супервизии процессов. - -instructions: | - - В этот раз допишите функции агента `Accumulator`, используя модуль `Task` и опираясь на модуль `Calculator`: - - `add` прибавить к состоянию аккумулятора переданное число; - - `sub` вычесть состояние аккумулятора на переданное число; - - `mul` умножить состояние аккумулятора на переданное число; - - `div` разделить состояние аккумулятора на переданное число; - - `reset` сбрасывает состояние `Accumulator` в ноль; - - `current` возвращает нынешнее состояние `Accumulator`. - - ```elixir - Accumulator.start_link(0) - - Accumulator.add(10) # => :ok - Accumulator.current() # => 10 - - Accumulator.sub(2) # => :ok - Accumulator.current() # => 8 - - Accumulator.mul(2) # => :ok - Accumulator.current() # => 16 - - Accumulator.div(4) # => :ok - Accumulator.current() # => 4 - - Accumulator.reset() # => :ok - Accumulator.current() # => 0 - ``` - -tips: - - | - [Официальная документация Agent](https://hexdocs.pm/elixir/Agent.html) - - | - [Официальная документация Task](https://hexdocs.pm/elixir/Task.html) diff --git a/modules/55-processes/40-tasks-and-agents/en/data.yml b/modules/55-processes/40-tasks-and-agents/en/data.yml index 75b0984..645bd74 100644 --- a/modules/55-processes/40-tasks-and-agents/en/data.yml +++ b/modules/55-processes/40-tasks-and-agents/en/data.yml @@ -1,2 +1,3 @@ +--- name: Tasks and agents tips: [] diff --git a/modules/55-processes/40-tasks-and-agents/ru/data.yml b/modules/55-processes/40-tasks-and-agents/ru/data.yml index 1baf822..deda6cf 100644 --- a/modules/55-processes/40-tasks-and-agents/ru/data.yml +++ b/modules/55-processes/40-tasks-and-agents/ru/data.yml @@ -1,3 +1,4 @@ +--- name: Задачи и агенты tips: - | diff --git a/modules/55-processes/50-supervisors/description.en.yml b/modules/55-processes/50-supervisors/description.en.yml deleted file mode 100644 index 00dd442..0000000 --- a/modules/55-processes/50-supervisors/description.en.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- -name: Supervisors -theory: | - - 🚧 In development - -instructions: | - - 🚧 In development - -tips: [] diff --git a/modules/55-processes/50-supervisors/description.ru.yml b/modules/55-processes/50-supervisors/description.ru.yml deleted file mode 100644 index be60eaa..0000000 --- a/modules/55-processes/50-supervisors/description.ru.yml +++ /dev/null @@ -1,180 +0,0 @@ ---- -name: Супервизоры -theory: | - - Супервизоры - процессы которые следят за жизненным циклом других процессов (потомков). Благодаря супервизорам появляется возможность выстраивать внутри приложения специальную иерархическую структуру, называемую *деревом супервизии*. Дерево супервизии позволяет выстраивать отказоустойчивы приложения, логика обработки и перезапуска процессов в котором скрыта от пользователя приложения. - - Супервизоры, как и дерево супервизии, являются частью OTP Erlang (Open Telecom Platform) - набором библиотек и подходов для упрощения работы с процессами. Далее по модулю мы познакомимся еще с одной важной сущностью OTP, помимо дерева супервизии и супервизоров. - - Для запуска супервизора нужно передать ему список потомков с их спецификацией через функцию `start_link`. Либо реализовать модуль с функциями обратного вызова, соответствующих поведению `Supervisor`. Теперь создадим агента-счетчик, который передадим под наблюдение: - - ```elixir - defmodule Counter do - use Agent - - def start_link(initial_state \\ 0) do - Agent.start_link(fn -> initial_state end, name: __MODULE__) - end - - def current_value do - Agent.get(__MODULE__, fn state -> state end) - end - - def inc do - Agent.update(__MODULE__, fn state -> state + 1 end) - end - - def dec do - Agent.update(__MODULE__, fn state -> state - 1 end) - end - - def boom do - raise "boom" - end - end - ``` - - При использовании поведения `Agent`, в модуль добавилась функция `child_spec/1`. Она и нужна для запуска агента через супервизора. У сгенерированной функции арность 1, для того, чтобы передать начальное состояние при инициализации агента. Вызовем эту функцию: - - ```elixir - Counter.child_spec(0) - # => %{id: Counter, start: {Counter, :start_link, [0]}} - - Counter.child_spec(5) - # => %{id: Counter, start: {Counter, :start_link, [5]}} - ``` - - Как видно из примеров, `child_spec/1` всего лишь возвращает словарь с конфигурацией процесса, идентификатором процесса выступает его модуль, под ключом `:start` находится информация о том, как запускать процесс. Перевести эту спецификацию можно следующим образом: в первом вызове `child_spec/1` процесс нужно запустить `Counter.start_link(0)`, во втором случае `Counter.start_link(5)`. - - Идея в том, что при запуске супервизору нужно передать список наблюдаемых процессов, в которых содержится кортеж с названием модулей и его начальным состоянием. А теперь запустим супервизора для наблюдения за агентом: - - ```elixir - children = [ - {Counter, 0} - ] - - {:ok, pid} = Supervisor.start_link(children, strategy: :one_for_all) - pid - # => #PID<0.127.0> - ``` - - Создался процесс-супервизор, который запустил агента и отслеживает его состояние. Можно и не указывать начальное состояние, передав список потомков без кортежей: - - ```elixir - children = [Counter] - - {:ok, pid} = Supervisor.start_link(children, strategy: :one_for_all) - # => #PID<0.129.0> - ``` - - Однако в таком случае, в `start_link/1` агента передастся пустой список: - - ```elixir - defmodule Counter do - use Agent - - def start_link(initial_state \\ 0) do - IO.inspect(initial_state) - Agent.start_link(fn -> initial_state end, name: __MODULE__) - end - end - - children = [Counter] - - {:ok, pid} = Supervisor.start_link(children, strategy: :one_for_all) - # => [] - # => #PID<0.131.0> - ``` - - В таких случаях нужно переписать инициализацию процесса таким образом: - - ```elixir - def start_link(opts) do - {initial_value, opts} = Keyword.pop(opts, :initial_value, 0) - Agent.start_link(fn -> initial_value end, opts) - end - ``` - - Теперь, когда процесс запущен, можно получить служебную информацию о супервизоре с помощью `count_children` и `which_children`, а так же пользоваться агентом по прямому назначению: - - ```elixir - {:ok, pid} = Supervisor.start_link([{Counter, 0}], strategy: :one_for_all) - - Supervisor.count_children(pid) - # => %{active: 1, workers: 1, supervisors: 0, specs: 1} - - Counter.inc() - Counter.inc() - Counter.current_value() - # => 2 - - Supervisor.which_children(pid) - # => [{Counter, #PID<0.146.0>, :worker, [Counter]}] - ``` - - Рассмотрим запуск супервизора `Supervisor.start_link(children, strategy: :one_for_all)`, а конкретно `strategy: :one_for_all`. Эта опция является стратегией перезапуска процессов которые завершились аварийно. Таких стратегий перезапуска несколько: - - `:one_for_one` - если потомок-процесс аварийно завершился, то только этот процесс и будет перезапущен; - - `:one_for_all` - если один из потомков аварийно завершился, то *все* дочерние процессы супервизора завершатся и перезапустятся; - - `:rest_for_one` - если один из потомков аварийно завершился, то только этот процесс и все следующие за ним потомки завершатся и перезапустятся. - - Зачастую используются стратегии `:one_for_one` и `:rest_for_one`. - - Помимо стратегии перезапуска, в супервизоре можно указать, каким образом завершать наблюдаемые процессы. А точнее, какой сигнал выхода отправить процессу при, например, перезапуске. Можно указать приоритет у процессов потомков, например, если есть какой-то важный процесс потомок, без которого супервизия не имеет смысла и после завершения работы этого потомка супервизор получает сигнал от этого процесса и супервизор завершает работу (process shutdown). Об этом можно подробнее почитать в [официальной документации](https://hexdocs.pm/elixir/Supervisor.html#module-supervisor-strategies-and-options). Тепрерь посмотрим на примере, как работает стратегия `:one_for_all`: - - ```elixir - {:ok, supervisor_pid} = Supervisor.start_link([{Counter, 0}], strategy: :one_for_one) - - Counter.inc() - Counter.current_value() - # => 1 - - # теперь вызовем функцию которая бросит исключение - Counter.boom() - # => ** (RuntimeError) boom - # => iex:71: Counter.boom/0 - # => iex:67: (file) - - Supervisor.count_children(supervisor_pid) - # => %{active: 1, workers: 1, supervisors: 0, specs: 1} - - Counter.current_value() - # => 0 - ``` - - Процесс `Counter` завершился с ошибкой, супервизор перехватил исключение и перезапустил процесс счетчик с начальным состоянием по умолчанию. - -instructions: | - - Супервизоры могут задаваться через модули, для этого нужно в модуле использовать поведение `Supervisor` и определить функцию обратного вызова `init/1`, которая будет вызвана при запуске супервизора через `start_link`. В файле с решением описано два агента, ваша задача дописать модуль `Solution`, добавив две функции: - - `start_link` - функция для запуска супервизора из модуля - - `init` - функция, которая запустится перед стартом супервизора, в котором указывается список потомков, стратегия перезапуска и т.д. - - В функции `init` сделайте потомками `Decrementor` и `Incrementor`, а в стратегии перезапуска укажите `:one_for_one`. - - ```elixir - {:ok, _} = Solution.start_link() - - Supervisor.which_children(Solution) - # => [ - # => {Decrementor, #PID<0.196.0>, :worker, [Decrementor]}, - # => {Incrementor, #PID<0.195.0>, :worker, [Incrementor]} - # => ] - - Supervisor.count_children(Solution) - # => %{active: 2, workers: 2, supervisors: 0, specs: 2} - - Incrementor.run() - Incrementor.current_value() - # => 1 - - Decrementor.current_value() - # => 0 - ``` - -tips: - - | - [Официальная документация Supervisor](https://hexdocs.pm/elixir/Supervisor.html) - - | - [Про дерево супервизии в Erlang](https://erlang.org/documentation/doc-4.9.1/doc/design_principles/sup_princ.html) - - | - [Пример модуля супервизора](https://hexdocs.pm/elixir/Supervisor.html#module-module-based-supervisors) diff --git a/modules/55-processes/50-supervisors/en/data.yml b/modules/55-processes/50-supervisors/en/data.yml index 0d3086f..d80f380 100644 --- a/modules/55-processes/50-supervisors/en/data.yml +++ b/modules/55-processes/50-supervisors/en/data.yml @@ -1,2 +1,3 @@ +--- name: Supervisors tips: [] diff --git a/modules/55-processes/50-supervisors/ru/data.yml b/modules/55-processes/50-supervisors/ru/data.yml index 1fea043..e5e5999 100644 --- a/modules/55-processes/50-supervisors/ru/data.yml +++ b/modules/55-processes/50-supervisors/ru/data.yml @@ -1,3 +1,4 @@ +--- name: Супервизоры tips: - > diff --git a/modules/55-processes/55-process-registration/description.en.yml b/modules/55-processes/55-process-registration/description.en.yml deleted file mode 100644 index 0051f78..0000000 --- a/modules/55-processes/55-process-registration/description.en.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- -name: Process registration -theory: | - - 🚧 In development - -instructions: | - - 🚧 In development - -tips: [] diff --git a/modules/55-processes/55-process-registration/description.ru.yml b/modules/55-processes/55-process-registration/description.ru.yml deleted file mode 100644 index cc35f3d..0000000 --- a/modules/55-processes/55-process-registration/description.ru.yml +++ /dev/null @@ -1,181 +0,0 @@ ---- -name: Регистрация процессов -theory: | - - Функция `spawn`, при вызове, возвращает идентификатор процесса: - - ```elixir - spawn(fn -> 2 + 2 end) - # => #PID<0.197.0> - ``` - - Однако, если процесс создается не напрямую, например через супервизора, то у программиста "пропадает" доступ к идентификатору процесса. Для таких случаев используется регистрация процессов. Вспомним код агента-счетчика из прошлых упражнений: - - ```elixir - defmodule Counter do - use Agent - - def start_link(initial_state \\ 0) do - Agent.start_link(fn -> initial_state end, name: __MODULE__) - end - - def current_value, do: Agent.get(__MODULE__, fn state -> state end) - def inc, do: Agent.update(__MODULE__, fn state -> state + 1 end) - def dec, do: Agent.update(__MODULE__, fn state -> state - 1 end) - end - ``` - - При вызове функции `Agent.start_link`, передается опция `:name`, в которой находится название нынешнего модуля, то есть `Counter`. В итоге после создания, процесс сохраняется в локальном регистре процессов под названием модуля `Counter`: - - ```elixir - Counter.start_link() - # => {:ok, #PID<0.115.0>} - ``` - - А теперь проверим список зарегистрированных процессов: - - ```elixir - Process.registered() - # => [:user_drv, :rex, :inet_db, :elixir_code_server, :erl_prim_loader, - # => :user_drv_writer, :global_name_server, :init, :kernel_sup, :code_server, - # => :erts_code_purger, :logger_std_h_default, :logger_sup, :socket_registry, - # => IEx.Config, :user_drv_reader, :erl_signal_server, :elixir_config, IEx.Pry, - # => :file_server_2, :standard_error_sup, Logger.Supervisor, :global_group, - # => :application_controller, :kernel_safe_sup, :user, :logger_proxy, IEx.Broker, - # => :logger, :global_group_check, Counter, IEx.Supervisor, :kernel_refc, - # => :elixir_sup, :logger_handler_watcher, :standard_error] - ``` - - Как видно из списка, при запуске консоли по умолчанию регистрируется множество разных процессов, но помимо них, в списке появился `Counter`. Благодаря этому, мы теперь можем обращаться к процессу через название модуля: - - ```elixir - # найдем процесс - pid = Process.whereis(Counter) - # => #PID<0.115.0> - - # обновим состояние агента напрямую - Agent.update(pid, fn state -> state + 100 end) - # => :ok - Counter.current_value() - # => 100 - ``` - - В примере выше, не стоит обновлять состояние агента напрямую через идентификатор процесса, так как это обход абстракций. Модуль представляет отдельные функции для работы с ним, которые скрывают детали реализации, поэтому ими и нужно пользоваться. А теперь попробуем еще раз запустить процесс-счетчик: - - ```elixir - Counter.start_link() - # => {:error, {:already_started, #PID<0.115.0>}} - ``` - - Процесс уже запущен и зарегистрирован, поэтому повторное создание не требуется. Попробуем убить процесс и сделаем пару проверок: - - ```elixir - pid = Process.whereis(Counter) - Process.exit(pid, :kill) - # => ** (EXIT from #PID<0.109.0>) shell process exited with reason: killed - # => Interactive Elixir (1.15.5) - press Ctrl+C to exit (type h() ENTER for help) - - Process.registered() - # => [:user_drv, :rex, :inet_db, :elixir_code_server, :erl_prim_loader, - # => :user_drv_writer, :global_name_server, :init, :kernel_sup, :code_server, - # => :erts_code_purger, :logger_std_h_default, :logger_sup, :socket_registry, - # => IEx.Config, :user_drv_reader, :erl_signal_server, :elixir_config, IEx.Pry, - # => :file_server_2, :standard_error_sup, Logger.Supervisor, :global_group, - # => :application_controller, :kernel_safe_sup, :user, :logger_proxy, IEx.Broker, - # => :logger, :global_group_check, IEx.Supervisor, :kernel_refc, :elixir_sup, - # => :logger_handler_watcher, :standard_error] - - Counter.current_value() - # => ** (exit) exited in: GenServer.call(Counter, {:get, #Function<0.123767520/1 in Counter.current_value/0>}, 5000) - # => ** (EXIT) no process: the process is not alive or there's no process currently associated with the given name, possibly because its application isn't started - # => (elixir 1.15.5) lib/gen_server.ex:1063: GenServer.call/3 - # => iex:39: (file) - - Process.whereis(Counter) - # => nil - ``` - - Сначала мы отправили сигнал агенту о завершении, но так как процесс агента был связан напрямую с процессом интерактивной оболочкой, при аварийном завершении процесса агента заодно завершился процесс оболочки. Произошло это из-за того, что процесс оболочки в данном случае выступал "супервизором" относительно процесса счетчика. Однако, из-за того, что процесс оболочки тоже находится под наблюдением другого супервизора, то при аварийном завершении оболочки, супервизор перехватил ошибку и перезапустил процесс интерактивной оболочки, но не перезапустил процесс счетчика. Счетчик не был перезапущен ровно потому, что о нем было известно только процессу оболочки и никаких стратегий перезапуска для счетчика не было указано при старте интерактивной оболочки. Ровно об этом и говорит код ниже, что счетчика, как процесса, больше не существует, его нет в регистре процессов и его нельзя менять. - - В базовом случае, процессы регистрируются через `Process.register`, однако процесс при регистрации должен быть обязательно живым: - - ```elixir - # долгоживущий процесс - pid = spawn(fn -> Process.sleep(:timer.seconds(20)) end) - #PID<0.118.0> - - Process.register(pid, :my_process) - # => true - - Process.registered() - # => [..., :my_process, ...] - - # спустя 20 секунд - Process.registered() - # => [...] процесса с именем :my_process нет - - # попробуем записать быстроживущий процесс - pid = spawn(fn -> 2 + 2 end) - # => #PID<0.119.0> - - Process.register(pid, :other_process) - # => ** (ArgumentError) could not register #PID<0.119.0> with name :other_process because it is not alive, the name is already taken, or it has already been given another name - # => (elixir 1.15.5) lib/process.ex:698: Process.register(#PID<0.119.0>, :other_process) - # => iex:42: (file) - ``` - - В целом это все что необходимо знать о регистрации процессов. Если нужно регистрировать несколько одинаковых процессов под разными именами, то нужно к этому подходить с осторожностью. Например, не генерировать атомы, так как они не вычищаются сборщиком мусора и в один момент приложение переполнится по памяти. - - Если стандартного регистра процессов не хватает, то можно сделать свой, указав, к примеру, каким образом разрешать конфликты имен и как в целом хранить информацию о зарегистрированных процессах. Об этом подробнее можно почитать уже в [официальной документации](https://hexdocs.pm/elixir/Registry.html). - -instructions: | - - Допишите агента, добавив функции `add`, `drop` для регистрации/снятия процессов с учета и функцию `list_registered`, которая выводит список процессов, которые зарегистрированы через агента. Если процесс завершился, то при вызове `add` регистрировать процесс не нужно. Если удаляемый процесс не существует, то нужно вернуть `:ok`, учтите, что `Process.unregister` вызывает исключение при попытке удалить несуществующий процесс. - - ```elixir - ProcessRegister.start_link() - # => {:ok, #PID<0.139.0>} - - ProcessRegister.list_registered() - # => %{} - - pid = spawn(fn -> Process.sleep(:timer.seconds(30)) end) - ProcessRegister.add(pid, :timeout_process) - # => :ok - pid = spawn(fn -> Process.sleep(:timer.seconds(30)) end) - ProcessRegister.add(pid, :other_timeout) - # => :ok - pid = spawn(fn -> Process.sleep(:timer.seconds(30)) end) - ProcessRegister.add(pid, :timeout) - # => :ok - - ProcessRegister.list_registered() - # => %{ - # => timeout: #PID<0.152.0>, - # => timeout_process: #PID<0.150.0>, - # => other_timeout: #PID<0.151.0> - # => } - - Process.registered() - # => [..., :timeout, :timeout_process, :other_timeout, ...] - - ProcessRegister.drop(:timeout_process) - # => :ok - ProcessRegister.list_registered() - # => %{ - # => timeout: #PID<0.152.0>, - # => other_timeout: #PID<0.151.0> - # => } - - Process.registered() - # => [..., :timeout, :other_timeout, ...] - - ProcessRegister.drop(:not_existing_process) - # => :ok - ``` - - Синхронизацию состояния агента и регистра реализовывать не нужно. - -tips: - - | - [Официальная документация](https://hexdocs.pm/elixir/Registry.html) diff --git a/modules/55-processes/55-process-registration/en/data.yml b/modules/55-processes/55-process-registration/en/data.yml index be05150..27bccbd 100644 --- a/modules/55-processes/55-process-registration/en/data.yml +++ b/modules/55-processes/55-process-registration/en/data.yml @@ -1,2 +1,3 @@ +--- name: Process registration tips: [] diff --git a/modules/55-processes/55-process-registration/ru/data.yml b/modules/55-processes/55-process-registration/ru/data.yml index d1f94ac..26694a3 100644 --- a/modules/55-processes/55-process-registration/ru/data.yml +++ b/modules/55-processes/55-process-registration/ru/data.yml @@ -1,3 +1,4 @@ +--- name: Регистрация процессов tips: - | diff --git a/modules/55-processes/60-genservers/description.en.yml b/modules/55-processes/60-genservers/description.en.yml deleted file mode 100644 index 8e81d16..0000000 --- a/modules/55-processes/60-genservers/description.en.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- -name: Genservers -theory: | - - 🚧 In development - -instructions: | - - 🚧 In development - -tips: [] diff --git a/modules/55-processes/60-genservers/description.ru.yml b/modules/55-processes/60-genservers/description.ru.yml deleted file mode 100644 index 4b1cde2..0000000 --- a/modules/55-processes/60-genservers/description.ru.yml +++ /dev/null @@ -1,249 +0,0 @@ ---- -name: Генсерверы -theory: | - - Генсерверы являются основным строительным кирпичиком OTP. Генсервер - это процесс, который может хранить состояние, выполнять какой-либо код, не обязательно синхронно и т.д. Отличие обычных процессов от обобщенных серверных процессов (general servers) в том, что разработчики OTP заранее подготовили все необходимое, что зачастую нужно для работы с процессом. Например, обработка ошибок, написание функций обратного вызова, встраивание в дерево супервизии. - - Возникает вопрос, а в чем разница между `Agent` и `GenServer`, ведь с первого взгляда они делают одно и то же. Разница в том, что абстракция `Agent` построена на основе `GenServer`, например в `Agent` управление состоянием происходит через функцию `Agent.update`, в то время как у `GenServer` состоянием нужно управлять вручную. Абстракция агентов построена вокруг какого-либо *состояния*, в то время как генсервер может и не обладать состоянием, а только выполнять какую-либо работу. - - На самом деле, в первых упражнениях мы уже написали некоторое подобие генсервера, которое работает по схеме: - - ```text - _______________ _____________ - |client module| <-> |server call| - --------------- ------------- - ^ - | - v - __________________ _____________ - |server callbacks| <-> |server loop| - ------------------ ------------- - ``` - - Модуль клиент и вызов сервера работают в потоке клиента, цикл сервера и его функции обратного вызова работают в потоке сервера. По сути так и работает генсервер. Теперь рассмотрим, как будет выглядеть счетчик генсервер: - - ```elixir - defmodule Counter do - use GenServer - - def init(initial_state \\ 0) do - {:ok, initial_state} - end - - def current, do: GenServer.call(__MODULE__, :current) - def inc, do: GenServer.cast(__MODULE__, :inc) - def dec, do: GenServer.cast(__MODULE__, :dec) - - def handle_call(:current, _from, state) do - {:reply, state, state} - end - - def handle_cast(:inc, state) do - {:noreply, state + 1} - end - - def handle_cast(:dec, state) do - {:noreply, state - 1} - end - end - ``` - - И проверим счетчик в работе: - - ```elixir - # создаем и регистрируем процесс генсервер - GenServer.start_link(Counter, 0, name: Counter) - # => {:ok, #PID<0.115.0>} - - Counter.current() - # => 0 - - Counter.inc() - # => :ok - Counter.inc() - # => :ok - Counter.inc() - # => :ok - Counter.current() - # => 3 - - Counter.dec() - # => :ok - Counter.dec() - # => :ok - Counter.current() - # => 1 - ``` - - Как и в случае с агентами, внутри генсервера мы описываем функции для клиентской части и серверной, вызов серверной части происходит во время обработки сообщения процесса генсервера, а конкретнее в функциях обратного вызова `handle_call`, `handle_cast` и `handle_info`. Эти функции действую следующим образом: - - `handle_call` - синхронный обработчик сообщения, когда клиенту важен ответ от сервера и он блокируется, в ожидании оного; - - `handle_cast` - асинхронный обработчик сообщения, когда клиент не хочет блокироваться и ждать ответа от сервера; - - `handle_info` - низкоуровневый асинхронный обработчик, срабатывает когда сообщение серверу передается напрямую, а не через `GenServer.call`, `GenServer.cast`. Зачастую используется для внутренних задач генсервера, например по таймеру обнулять состояние генсервера или сделать http вызов. Еще можно использовать этот обработчик в качестве мониторинга за другим процессом через обработку сообщения `:DOWN`. - - Так как `GenServer` является поведением, то мы можем перед описанием функции обратного вызова добавить аттрибут `@impl true`. Это позволит компилятору проверить сигнатуры описанных функций и что необходимые функции вообще реализованы. Подробнее про возвращаемые значения для функций обратных вызовов можно посмотреть в [официальной документации](https://hexdocs.pm/elixir/GenServer.html#c:handle_cast/2), либо в интерактивной консоли ввести следующую команду: - - ```elixir - iex(48)> b GenServer - @callback code_change(old_vsn, state :: term(), extra :: term()) :: - {:ok, new_state :: term()} | {:error, reason :: term()} - when old_vsn: term() | {:down, term()} - - @callback format_status(reason, pdict_and_state :: list()) :: term() - when reason: :normal | :terminate - - @callback handle_call(request :: term(), from(), state :: term()) :: - {:reply, reply, new_state} - | {:reply, reply, new_state, - timeout() | :hibernate | {:continue, continue_arg :: term()}} - | {:noreply, new_state} - | {:noreply, new_state, - timeout() | :hibernate | {:continue, continue_arg :: term()}} - | {:stop, reason, reply, new_state} - | {:stop, reason, new_state} - when reply: term(), new_state: term(), reason: term() - # .............. - ``` - - Важно понимать, что обработчики `handle_call`, `handle_cast` и `handle_info` при определении завязываются на конкретные сообщения, которые они обрабатывают, поэтому важно описать паттерн catch all, который перехватит сообщения, которые генсервер не знает как обработать. Если же не описать обобщенный вариант обработки сообщения, то процесс генсервер аварийно завершится при таком вызове: - - ```elixir - GenServer.call(Counter, :do_something_different) - # => 15:30:36.108 [error] GenServer Counter terminating - # => ** (FunctionClauseError) no function clause matching in Counter.handle_call/3 - # => iex:12: Counter.handle_call(:do_something_different, {#PID<0.109.0>, [:alias | #Reference<0.0.13955.4010078260.1056243713.84046>]}, 0) - # => (stdlib 5.0.2) gen_server.erl:1113: :gen_server.try_handle_call/4 - # => (stdlib 5.0.2) gen_server.erl:1142: :gen_server.handle_msg/6 - # => (stdlib 5.0.2) proc_lib.erl:241: :proc_lib.init_p_do_apply/3 - ``` - - Поэтому допишем общие обработчики, которые перехватят любое сообщение: - - ```elixir - defmodule Counter do - use GenServer - - def init(initial_state \\ 0) do - {:ok, initial_state} - end - - # other code... - @impl true - def handle_call(msg, _from, state) do - IO.puts("handle_call intercept unknown message: #{inspect(msg)}") - {:reply, {:error, :unknown_msg}, state} - end - - @impl true - def handle_cast(msg, state) do - IO.puts("handle_cast intercept unknown message: #{inspect(msg)}") - {:noreply, state} - end - end - ``` - - Теперь проверим в работе: - - ```elixir - GenServer.call(Counter, :some) - # => handle_call intercept unknown message: :some - # => {:error, :unknown_msg} - - GenServer.cast(Counter, :some) - # => handle_cast intercept unknown message: :some - # => :ok - - GenServer.call(Counter, :current) - # => 0 - ``` - - В случае с `handle_info` нет необходимости отдельно прописывать функцию обратного вызова, так как она добавляется при вызове `use GenServer`: - - ```elixir - send(Counter, :my_msg) - # => :my_msg - # => 15:57:56.871 [error] Counter Counter received unexpected message in handle_info/2: :my_msg - # однако процесс не завершился - - Process.alive?(Process.whereis(Counter)) - # => true - ``` - - Помимо предотвращения аварийных ситуаций, общий шаблон обработки сообщений не позволит почтовому ящику генсервера переполнится, так как в BEAM нет никаких ограничений на количество сообщений в ящике процесса. А сообщения, под которые нет общих обработчиков продолжат висеть в очереди почтового ящика до тех пор, пока память не переполнится и не сработает OOM-killer операционной системы, который аварийно завершит весь узел BEAM. Однако, иногда переполнение может возникнуть из-за медленной скорости обработки сообщений, в таких случаях используют пул генсерверов, которые будут вести обработку сообщений параллельно. - - Есть еще обработчик `terminate`, он срабатывает при завершении генсервера, например, в этом обработчике можно подчистить какие-либо ресурсы или что-то логгировать. Однако этот обработчик используется редко и вызывается он если только пометить процесс генсервера как системный, то есть через `Process.flag(:trap_exit, true)`. В целом, если нет какой-либо острой нужды в сохранении состояния или освобождения ресурсов после завершения процесса, то использовать этот обработчик нет необходимости. - - Мы рассмотрели необходимый минимум, чтобы понимать, что такое процессы в Elixir экосистеме, что такое OTP и как все это использовать. Познакомились с абстракциями `Supervisor`, `GenServer`, `Agent` и узнали о дереве супервизии. Для еще лучшего понимания, какие идеи стоят за процессами, стоит почитать про акторную модель и историю языка Erlang. - -instructions: | - - В файле с решением описана инициализация генсервера-кеша, ваша задача дописать модуль `Solution`, добавив следующие функции: - - `add/2` - функция добавляющая в кеш ключ-значение, асинхронная; - - `drop/1` - функция, удаляющая из кеша значение по переданному ключу, асинхронная; - - `reset/0` - функция, сбрасывающая состояние генсервера, асинхронная; - - `current_state/0` - функция, возвращающая состояние генсервера, синхронная; - - `has?/1` - функция, проверяющая, есть ли переданный ключ в состоянии генсервера, синхронная. - - Если не понятно, использовать `handle_cast` или `handle_call`, то изучите семантику реализуемых функций, нужен ли ответ от генсервера или нет. - - ```elixir - Solution.start_link() - # => {:ok, #PID<0.121.0>} - - Solution.current_state() - # => %{} - - Solution.add(:my_key, "value") - # => :ok - - Solution.current_state() - # => %{my_key: "value"} - - Solution.has?(:my_key) - # => true - - Solution.has?(:other_key) - # => false - - Solution.drop(:not_existing_key) - # => :ok - - Solution.current_state() - # => %{my_key: "value"} - - Solution.drop(:my_key) - # => :ok - - Solution.current_state() - # => %{} - - 1..10 |> Enum.each(&(Solution.add("key_#{&1}", "value_#{&1}"))) - - Solution.current_state() - # => %{ - # => "key_1" => "value_1", - # => "key_10" => "value_10", - # => "key_2" => "value_2", - # => "key_3" => "value_3", - # => "key_4" => "value_4", - # => "key_5" => "value_5", - # => "key_6" => "value_6", - # => "key_7" => "value_7", - # => "key_8" => "value_8", - # => "key_9" => "value_9" - # => } - - Solution.reset() - # => :ok - Solution.current_state() - # => %{} - ``` - -tips: - - | - [Официальная документация GenServer](https://hexdocs.pm/elixir/GenServer.html) - - | - [Акторная модель](https://ru.wikipedia.org/wiki/%D0%9C%D0%BE%D0%B4%D0%B5%D0%BB%D1%8C_%D0%B0%D0%BA%D1%82%D0%BE%D1%80%D0%BE%D0%B2#:~:text=%D0%9C%D0%BE%D0%B4%D0%B5%D0%BB%D1%8C%20%D0%B0%CC%81%D0%BA%D1%82%D0%BE%D1%80%D0%BE%D0%B2%20%E2%80%94%20%D0%BC%D0%B0%D1%82%D0%B5%D0%BC%D0%B0%D1%82%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%B0%D1%8F%20%D0%BC%D0%BE%D0%B4%D0%B5%D0%BB%D1%8C%20%D0%BF%D0%B0%D1%80%D0%B0%D0%BB%D0%BB%D0%B5%D0%BB%D1%8C%D0%BD%D1%8B%D1%85,%D1%81%D1%87%D0%B8%D1%82%D0%B0%D1%8E%D1%89%D0%B5%D0%B3%D0%BE%D1%81%D1%8F%20%D1%83%D0%BD%D0%B8%D0%B2%D0%B5%D1%80%D1%81%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D0%BC%20%D0%BF%D1%80%D0%B8%D0%BC%D0%B8%D1%82%D0%B8%D0%B2%D0%BE%D0%BC%20%D0%BF%D0%B0%D1%80%D0%B0%D0%BB%D0%BB%D0%B5%D0%BB%D1%8C%D0%BD%D0%BE%D0%B3%D0%BE%20%D0%B8%D1%81%D0%BF%D0%BE%D0%BB%D0%BD%D0%B5%D0%BD%D0%B8%D1%8F.) - - | - [История Erlang](https://habr.com/ru/companies/ruvds/articles/749192/) - - | - [Цитаты Джо Армстронга, создателя Erlang](https://habr.com/ru/articles/508696/) diff --git a/modules/55-processes/60-genservers/en/data.yml b/modules/55-processes/60-genservers/en/data.yml index 2ed84c9..d868836 100644 --- a/modules/55-processes/60-genservers/en/data.yml +++ b/modules/55-processes/60-genservers/en/data.yml @@ -1,2 +1,3 @@ +--- name: Genservers tips: [] diff --git a/modules/55-processes/60-genservers/ru/data.yml b/modules/55-processes/60-genservers/ru/data.yml index 4d69670..3b96c91 100644 --- a/modules/55-processes/60-genservers/ru/data.yml +++ b/modules/55-processes/60-genservers/ru/data.yml @@ -1,3 +1,4 @@ +--- name: Генсерверы tips: - > diff --git a/modules/60-macros/10-macros-intro/description.en.yml b/modules/60-macros/10-macros-intro/description.en.yml deleted file mode 100644 index 9a5429c..0000000 --- a/modules/60-macros/10-macros-intro/description.en.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- -name: Intoduction to macros -theory: | - - 🚧 In development - -instructions: | - - 🚧 In development - -tips: [] diff --git a/modules/60-macros/10-macros-intro/description.ru.yml b/modules/60-macros/10-macros-intro/description.ru.yml deleted file mode 100644 index 90c2ef6..0000000 --- a/modules/60-macros/10-macros-intro/description.ru.yml +++ /dev/null @@ -1,88 +0,0 @@ ---- -name: Знакомство с макросами -theory: | - - Иногда возникает необходимость расширить сам язык, создав предметно-ориентированный язык (DSL), либо реализовать какие-то новые возможности. Elixir с помощью макросов позволяет достичь этого, а сам процесс написания макросов называется метапрограммированием. - - Макросы похожи на функции, но действуют совершенно иначе, чем функции. Объявляется макрос с помощью инструкции `defmacro`. Для начала рассмотрим отличия макросов и функций: - - ```elixir - # объявим модуль с функцией и макросом - defmodule Exercise do - def my_fn(x) do - IO.inspect(x) - x - end - - defmacro my_macro(x) do - IO.inspect(x) - x - end - end - ``` - - Чтобы воспользоваться макросом, нужно подключить модуль с помощью инструкции `require` в котором макрос объявлен: - - ```elixir - require Exercise - - Exercise.my_fn(1 + 2) - # => 3 - # => 3 - - Exercise.my_macro(1 + 2) - # => {:+, [line: 12], [1, 2]} - # => 3 - ``` - - Вызов функции понятен, Elixir вычисляет значение выражения `1 + 2`, затем передает его дальше в функцию, где полученное значение выводится на экран и возвращается из функции. В случае с макросом, вместо вычисления выражения, оно интерпретируется как кортеж, затем этот кортеж выводится на экран и возвращается из макроса, а после интерпретатор Elixir вычисляет возвращенный кортеж. - - Рассмотрим внимательнее кортеж, который оказался в макросе `{:+, [line: 12], [1, 2]}`, где `:+` - оператор, `[line: 12]` - метаданные об операции, `[1, 2]` - список операндов. Теперь, обладая пониманием, что означает этот кортеж, напишем макрос, который удваивает переданный аргумент: - - ```elixir - defmodule Exercise do - defmacro double(x) do - {:*, [], [2, x]} - end - end - - require Exercise - - Exercise.double(2) - # => 4 - Exercise.double(10) - # => 20 - Exercise.double(2 * 2) - # => 8 - Exercise.double(2 + 1 - 5) - # => -4 - ``` - - Макрос работает, но выглядит неудобно, в следующем упражнении рассмотрим как сделать макрос более читаемым. - - Интересный факт: создатель языка, Жозе Валим, при добавлении макросов в Elixir, вдохновлялся Clojure. - -instructions: | - - Создайте макрос `my_abs`, который берет абсолютное значение переданного аргумента (поможет функция `abs`): - - ```elixir - require Solution - - Solution.my_abs(-2) - # => 2 - Solution.my_abs(2) - # => 2 - Solution.my_abs(-5 * 100) - # => 500 - Solution.my_abs(-2 - 100 + 1) - # => 101 - ``` - -tips: - - | - [Официальная документация](https://hexdocs.pm/elixir/Macro.html) - - | - [Про DSL](https://ru.wikipedia.org/wiki/%D0%9F%D1%80%D0%B5%D0%B4%D0%BC%D0%B5%D1%82%D0%BD%D0%BE-%D0%BE%D1%80%D0%B8%D0%B5%D0%BD%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%BD%D1%8B%D0%B9_%D1%8F%D0%B7%D1%8B%D0%BA) - - | - [Про метапрограммирование](https://ru.wikipedia.org/wiki/%D0%9C%D0%B5%D1%82%D0%B0%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5) diff --git a/modules/60-macros/10-macros-intro/en/data.yml b/modules/60-macros/10-macros-intro/en/data.yml index 3636fe8..b3042af 100644 --- a/modules/60-macros/10-macros-intro/en/data.yml +++ b/modules/60-macros/10-macros-intro/en/data.yml @@ -1,2 +1,3 @@ +--- name: Intoduction to macros tips: [] diff --git a/modules/60-macros/10-macros-intro/ru/data.yml b/modules/60-macros/10-macros-intro/ru/data.yml index 7d9a6c6..2a0ed7b 100644 --- a/modules/60-macros/10-macros-intro/ru/data.yml +++ b/modules/60-macros/10-macros-intro/ru/data.yml @@ -1,3 +1,4 @@ +--- name: Знакомство с макросами tips: - | diff --git a/modules/60-macros/20-quoting/description.en.yml b/modules/60-macros/20-quoting/description.en.yml deleted file mode 100644 index 65fb2a2..0000000 --- a/modules/60-macros/20-quoting/description.en.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- -name: Intoduction to quote and unquote -theory: | - - 🚧 In development - -instructions: | - - 🚧 In development - -tips: [] diff --git a/modules/60-macros/20-quoting/description.ru.yml b/modules/60-macros/20-quoting/description.ru.yml deleted file mode 100644 index ff4cd02..0000000 --- a/modules/60-macros/20-quoting/description.ru.yml +++ /dev/null @@ -1,136 +0,0 @@ ---- -name: Знакомство с quote и unquote -theory: | - - Вспомним пример из прошлого упражнения: - - ```elixir - defmodule Exercise do - defmacro double(x) do - {:*, [], [2, x]} - end - end - ``` - - В этом макросе мы вернули код во `внутреннем представлении` языка Elixir, но работать с таким представлением неудобно, особенно если есть вложенность: - - ```elixir - # в этом макросе выполняется следующая операция ((x * 4) + 3) * x - defmodule Exercise do - defmacro magic(x) do - {:*, [], - [ - {:+, [], - [{:*, [], [x, 4]}, 3]}, - x - ] - } - end - end - - require Exercise - - Exercise.magic(1) - # 7, т.к. ((1 * 4) + 3) * 1 - Exercise.magic(2) - # 22, т.к. ((2 * 4) + 3) * 2 - ``` - - В примере выше мы напрямую управляли структурой AST дерева, однако понимать этот код стало сложнее. Для упрощения работы с AST в макросах Elixir есть функции `quote` и `unquote` прямиком перекочевавшие из Lisp-подобных языков. - - Функция `quote` принимает произвольное выражение на языке Elixir и преобразует его во внутренне представление языка: - - ```elixir - quote do: 1 + 2 - # => {:+, [], [1, 2]} - - quote do: Integer.to_string((1 + 2) * 3) - # => {{:., [], []}, [], - # => [ - # => {:*, [], - # => [{:+, [], [1, 2]}, 3]} - # => ]} - ``` - - Теперь попробуем переписать макрос из начала упражнения: - - ```elixir - defmodule Exercise do - defmacro double(x) do - quote do: 2 * x - end - end - - require Exercise - - Exercise.double(2) - # => error: undefined variable "x" (context Exercise) - ``` - - Код не работает из-за того, что `x` при передаче в макрос уже в форме внутреннего представления, поэтому нужно сообщить Elixir, что аргумент не нужно переводить во внутренне представление, для этого используется функция `unquote`: - - ```elixir - defmodule Exercise do - defmacro double(x) do - quote do - 2 * unquote(x) - end - end - end - - require Exercise - - Exercise.double(2) - # => 4 - ``` - - По сути мы сообщили Elixir следующее: `Преврати 2 * x во внутреннее представление, но оставь аргумент x в исходном виде.` - - Подведем итоги: `quote` означает `преврати все в блоке do в формат внутреннего представления`, `unquote` означает `не превращай это во внутренний формат`. - - Многие, кто пишут макросы, частно допускают ошибку, забывая передать аргументы функции `unquote`. Важно помнить, что *все* аргументы передаются макросам во внутреннем формате. - - Так же важно знать, что при передаче в `quote` атома, числа, строки, списка или кортежа с двумя элементами, функция вернет аргумент без изменений, а не кортеж во внутреннем формате. - - ```elixir - quote do: :a - # => :a - quote do: 2 - # => 2 - quote do: "hello" - # => "hello - quote do: [1, 2, 3] - # => [1, 2, 3] - - quote do: {1, 2} - # => {1, 2} - quote do: {1, 2, 3} - # => {:{}, [], [1, 2, 3]} - quote do: %{a: 2} - # => {:%{}, [], [a: 2]} - ``` - - Происходит это потому, что элементы из примера выше тоже представляют собой часть AST структуры Elixir, поэтому их не нужно дополнительно переводить во внутреннее представление. - -instructions: | - - Создайте макрос `my_unless`, который повторяет семантику `unless`: - - ```elixir - require Solution - - Solution.my_unless(false, do: 1 + 3) - # => 4 - Solution.my_unless(true, do: 1 + 3) - # => nil - Solution.my_unless(2 == 2, do: "hello") - # => nil - Solution.my_unless(2 == 1, do: "world") - # => "world" - ``` - -tips: - - | - [Официальная документация](https://hexdocs.pm/elixir/Kernel.SpecialForms.html#quote/2) - - | - [Про AST](https://ru.wikipedia.org/wiki/%D0%90%D0%B1%D1%81%D1%82%D1%80%D0%B0%D0%BA%D1%82%D0%BD%D0%BE%D0%B5_%D1%81%D0%B8%D0%BD%D1%82%D0%B0%D0%BA%D1%81%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%BE%D0%B5_%D0%B4%D0%B5%D1%80%D0%B5%D0%B2%D0%BE) diff --git a/modules/60-macros/20-quoting/en/data.yml b/modules/60-macros/20-quoting/en/data.yml index 03d6d99..b754479 100644 --- a/modules/60-macros/20-quoting/en/data.yml +++ b/modules/60-macros/20-quoting/en/data.yml @@ -1,2 +1,3 @@ +--- name: Intoduction to quote and unquote tips: [] diff --git a/modules/60-macros/20-quoting/ru/data.yml b/modules/60-macros/20-quoting/ru/data.yml index 5a20b4c..fc99015 100644 --- a/modules/60-macros/20-quoting/ru/data.yml +++ b/modules/60-macros/20-quoting/ru/data.yml @@ -1,3 +1,4 @@ +--- name: Знакомство с quote и unquote tips: - > diff --git a/modules/60-macros/30-new-functionality/description.en.yml b/modules/60-macros/30-new-functionality/description.en.yml deleted file mode 100644 index 5098cd1..0000000 --- a/modules/60-macros/30-new-functionality/description.en.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- -name: New functionality -theory: | - - 🚧 In development - -instructions: | - - 🚧 In development - -tips: [] diff --git a/modules/60-macros/30-new-functionality/description.ru.yml b/modules/60-macros/30-new-functionality/description.ru.yml deleted file mode 100644 index a5f7c53..0000000 --- a/modules/60-macros/30-new-functionality/description.ru.yml +++ /dev/null @@ -1,110 +0,0 @@ ---- -name: Новые возможности -theory: | - - Макросы позволяют добавлять в язык новые команды, как в предыдущем упражнении: - - ```elixir - defmodule Solution do - defmacro my_unless(condition, do: expression) do - quote do - if(!unquote(condition), do: unquote(expression)) - end - end - end - ``` - - На самом деле, в Elixir все конструкции имеют внутреннее представление, даже функции. Это означает, что с помощью макроса можно сгенерировать функцию: - - ```elixir - defmodule Exercise do - defmacro create_multiplier(fn_name, factor) do - quote do - def unquote(fn_name)(value) do - unquote(factor) * value - end - end - end - end - ``` - - Чтобы воспользоваться этим макросом, нужно создать другой модуль, так как определение функции доступно только внутри модуля: - - ```elixir - defmodule MyModule do - require Exercise - - Exercise.create_multiplier(:double, 2) - Exercise.create_multiplier(:triple, 3) - - def run_example() do - x = double(2) - IO.puts("Two times 2 is #{x}") - end - end - - MyModule.double(5) - # => 10 - MyModule.triple(3) - # => 9 - MyModule.run_example() - # => Two times 2 is 4 - ``` - - Создадим универсальный макрос, который за один вызов создает нужное количество функций: - - ```elixir - defmodule Exercise do - defmacro create_functions(fn_list) do - Enum.map(fn_list, fn {name, factor} -> - quote do - def unquote(:"#{name}_value")(value) do - unquote(factor) * value - end - end - end) - end - end - ``` - - И теперь опробуем макрос в новом модуле: - - ```elixir - defmodule Example do - require Exercise - - Exercise.create_functions([{:double, 2}, {:triple, 3}, {:nullify, 0}]) - end - - Example.double_value(2) - # => 4 - Example.triple_value(2) - # => 6 - Example.nullify_value(2) - # => 0 - ``` - - Если нужно создать макрос только внутри модуля, то `defmacrop` то что нужно. Как и приватные функции, макрос объявленный таким образом, будет доступен только в модуле, где макрос объявлен и только во время компиляции. - -instructions: | - - Создайте макрос `prohibit_words`, генерирующий функцию `forbidden?`, в который передается список запрещенных слов и проверяется, запрещено ли слово, переданное в функцию `forbidden?`. Если передано не слово, то функция возвращает `false`: - - ```elixir - defmodule Exercise - require Solution - - Solution.prohibit_words(["hello", "world", "foo"]) - end - - Exercise.forbidden?("hello") - # => true - Exercise.forbidden?("test") - # => false - Exercise.forbidden?(1) - # => false - Exercise.forbidden?(%{hello: :world}) - # => false - ``` - -tips: [] diff --git a/modules/60-macros/30-new-functionality/en/data.yml b/modules/60-macros/30-new-functionality/en/data.yml index a332a10..f90b62a 100644 --- a/modules/60-macros/30-new-functionality/en/data.yml +++ b/modules/60-macros/30-new-functionality/en/data.yml @@ -1,2 +1,3 @@ +--- name: New functionality tips: [] diff --git a/modules/60-macros/30-new-functionality/ru/data.yml b/modules/60-macros/30-new-functionality/ru/data.yml index 925b1d3..f5d9a73 100644 --- a/modules/60-macros/30-new-functionality/ru/data.yml +++ b/modules/60-macros/30-new-functionality/ru/data.yml @@ -1,2 +1,3 @@ +--- name: Новые возможности tips: [] diff --git a/modules/60-macros/40-macros-hygiene/description.en.yml b/modules/60-macros/40-macros-hygiene/description.en.yml deleted file mode 100644 index a23589b..0000000 --- a/modules/60-macros/40-macros-hygiene/description.en.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- -name: Macros hygiene -theory: | - - 🚧 In development - -instructions: | - - 🚧 In development - -tips: [] diff --git a/modules/60-macros/40-macros-hygiene/description.ru.yml b/modules/60-macros/40-macros-hygiene/description.ru.yml deleted file mode 100644 index ae165e1..0000000 --- a/modules/60-macros/40-macros-hygiene/description.ru.yml +++ /dev/null @@ -1,147 +0,0 @@ ---- -name: Гигиена макросов -theory: | - - При создании макросов важно соблюдать их гигиену, так как макросы, которые модифицируют окружение, могут сильно навредить. Негигиеничные макросы еще сложнее понимать и отлаживать, потому что они меняют окружение, в котором исполняется код. Для устранения таких проблем в разных языках добавлены специальные возможности по упрощению создания гигиеничных макросов, Elixir в том числе. - - По умолчанию, в Elixir макросы гигиеничны, поэтому можно не переживать за контекст, в котором макрос используется: - - ```elixir - defmodule Example do - defmacro no_interference do - quote do: a = 1 - end - end - - defmodule HygieneTest do - def run() do - require Example - a = 13 - Example.no_interference() - a - end - end - - HygieneTest.run() - # => 13 - ``` - - В примере выше, макрос не перезаписывает `a`, происходит это потому, что Elixir аннотирует переменные контекстом, в котором они объявлены: - - ```elixir - defmodule Sample do - def quoted do - quote do: x - end - end - - Sample.quoted() - #=> {:x, [line: 3], Sample} - ``` - - Благодаря аннотации, Elixir понимает к какому контексту переменные принадлежат. - - В очень редких случаях, когда иначе никак, макрос можно сделать негигиеничным, например с помощью `var!`: - - ```elixir - defmodule Example do - defmacro interference do - quote do: var!(a) = 1 - end - end - - defmodule HygieneTest do - def run() do - require Example - a = 13 - Example.interference() - a - end - end - - HygieneTest.run() - # => 1 - ``` - - Так делать можно только в *исключительных* случаях, будьте крайне осторожны. - - Иногда, нужно динамически объявить переменную, тогда воспользуемся `var`: - - ```elixir - defmodule Exercise do - defmacro initialize_to_char_count(variables) do - Enum.map(variables, fn name -> - var = Macro.var(name, nil) - length = name |> Atom.to_string |> String.length - - quote do - unquote(var) = unquote(length) - end - end) - end - - def run do - initialize_to_char_count [:red, :green, :yellow] - [red, green, yellow] - end - end - - Exercise.run() - # => [3, 4, 5] - ``` - - Обратите внимание, что передается вторым аргументом в `var`. Это контекст объявления переменной, благодаря которому пересечения переменных не произойдет, если они были объявлены ранее. - - Есть магическая структура `__ENV__`, которая хранит всю информацию о *скомпилированном* окружении, включая модули, файлы, переменные, импорты и так далее: - - ```elixir - __ENV__.module - # => nil - - __ENV__.file - # => "iex" - - __ENV__.requires - # => [Application, Exercise, IEx.Helpers, Kernel, Kernel.Typespec, Solution] - - require Integer - __ENV__.requires - # => [Application, Exercise, IEx.Helpers, Integer, Kernel, Kernel.Typespec, Solution] - ``` - - Большинство функций, которые используются в модуле `Macro`, взаимодействуют с этим окружением. - -instructions: | - - Создайте макрос `with_logging`, который принимает функцию, логгирует результат выполнения и возвращает результат. Примеры использования: - - ```elixir - defmodule Exercise - require Solution - - def run_fn(function) do - Solution.with_logging do - function - end - end - end - - Exercise.run_fn(fn -> 1 + 5 end) - # => Started execution... - # => Execution result is: 6 - # => 6 - - - Exercise.run_fn(fn -> %{hello: :world} end) - # => Started execution... - # => Execution result is: %{hello: :world} - # => %{hello: :world} - ``` - -tips: - - | - [Статья про гигиену макросов](https://en.wikipedia.org/wiki/Hygienic_macro) - - | - [Официальная документация](https://hexdocs.pm/elixir/Kernel.html#var!/2) - - | - [Официальная документация про Env](https://hexdocs.pm/elixir/Macro.Env.html) diff --git a/modules/60-macros/40-macros-hygiene/en/data.yml b/modules/60-macros/40-macros-hygiene/en/data.yml index db057d7..687f064 100644 --- a/modules/60-macros/40-macros-hygiene/en/data.yml +++ b/modules/60-macros/40-macros-hygiene/en/data.yml @@ -1,2 +1,3 @@ +--- name: Macros hygiene tips: [] diff --git a/modules/60-macros/40-macros-hygiene/ru/data.yml b/modules/60-macros/40-macros-hygiene/ru/data.yml index 51ea32e..d571e34 100644 --- a/modules/60-macros/40-macros-hygiene/ru/data.yml +++ b/modules/60-macros/40-macros-hygiene/ru/data.yml @@ -1,3 +1,4 @@ +--- name: Гигиена макросов tips: - | diff --git a/modules/60-macros/50-macros-ast/description.en.yml b/modules/60-macros/50-macros-ast/description.en.yml deleted file mode 100644 index 6d19614..0000000 --- a/modules/60-macros/50-macros-ast/description.en.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- -name: AST and final thoughts -theory: | - - 🚧 In development - -instructions: | - - 🚧 In development - -tips: [] diff --git a/modules/60-macros/50-macros-ast/description.ru.yml b/modules/60-macros/50-macros-ast/description.ru.yml deleted file mode 100644 index e0a7768..0000000 --- a/modules/60-macros/50-macros-ast/description.ru.yml +++ /dev/null @@ -1,134 +0,0 @@ ---- -name: AST и подведение итогов -theory: | - - В Elixir есть функции, упрощающих работу с абстрактным синтаксическим деревом (АСТ) языка. Например: - - ```elixir - module = """ - defmodule Example do - def sum(a, b) do - a + b - end - end - """ - - {:ok, result} = Code.string_to_quoted(module) - result - # => {:defmodule, [line: 1], - # => [ - # => {:__aliases__, [line: 1], [:Example]}, - # => [ - # => do: {:def, [line: 2], - # => [ - # => {:sum, [line: 2], [{:a, [line: 2], nil}, {:b, [line: 2], nil}]}, - # => [do: {:+, [line: 3], [{:a, [line: 3], nil}, {:b, [line: 3], nil}]}] - # => ]} - # => ] - # => ]} - - quoted = quote do: 1 + 2 - - quoted |> Macro.to_string |> IO.puts - # => 1 + 2 - # => :ok - - quoted_pipe = quote do: 100 |> div(10) |> div(5) - Macro.unpipe(quoted_pipe) - # => [ - # => {100, 0}, - # => {{:div, [context: Elixir, imports: [{2, Kernel}]], [10]}, 0}, - # => {{:div, [context: Elixir, imports: [{2, Kernel}]], [5]}, 0} - # => ] - ``` - - Теперь подведем итоги модуля. Мы рассмотрели, как с помощью макросов можно расширять возможности языка и напрямую работать с АСТ. - - Однако, в большинстве случаев прибегать к макросам является плохой затеей: - - Макросы сложнее понимать; - - Макросы сложнее отлаживать; - - Макросы притягивают макросы, то есть приходится писать дополнительные обвязки-макросы. - - Перед тем как написать, задумайтесь, есть ли возможность решить задачу с помощью функции? Ответ на этот вопрос почти всегда будет - `да`. - - Хоть Elixir помогает и одновременно ограничивает разработчиков, например: - - Макросы по умолчанию гигиеничны; - - Макросы не дают возможности глобально внедрять произвольный код, то есть необходимо напрямую в *конкретном* модуле вызвать `require` или `import` нужного макроса; - - Использование макросов в коде *явно*, поэтому неожиданного поведения, как например, полного переопределения функции, за спиной у разработчика не происходит; - - Использование явных `quote` и `unqoute` тоже упрощает понимание макроса, так как сразу видно, что будет выполнено сразу, а что потом. - - Но даже эти механизмы не снимают ответственность с разработчика за написанный им код, пишите макросы ответственно и вдумчиво, и только если в этом есть острая *необходимость*! - -instructions: | - - Создайте функцию `collect_module_stats`, которая принимает строку, в которой определяется модуль и функции модуля, а затем подсчитывается статистика по функциям, которые определены. - - Для начала, изучите функцию `string_to_quoted` модуля `Code` и функцию `prewalk` из модуля `Macro`. Формат собираемой статистики представлен в примерах: - - ```elixir - new_module = """ - defmodule MyModule do - - end - """ - - Solution.collect_module_stats(new_module) - # => [] - - new_module = """ - defmodule MyModule do - def hello() do - "world" - end - end - """ - - Solution.collect_module_stats(new_module) - # => [%{arity: 0, name: :hello}] - - new_module = """ - defmodule MyModule do - def hello() do - "world" - end - - defp test(a, b) do - a + b - end - end - """ - - Solution.collect_module_stats(new_module) - # => [%{arity: 2, name: :test}, %{arity: 0, name: :hello}] - - new_module = """ - defmodule MyModule do - def hello(string) do - [string, "world"] - end - - def magic(a, b, c) do - (a + b) * c - end - - defp test(a, b) do - a + b - end - end - """ - - Solution.collect_module_stats(new_module) - # => [%{arity: 2, name: :test}, %{arity: 3, name: :magic}, %{arity: 1, name: :hello}] - ``` - -tips: - - | - [Официальная документация Code](https://hexdocs.pm/elixir/Code.html) - - | - [Официальная документация Macro](https://hexdocs.pm/elixir/Macro.html#prewalk/2) - - | - [Про AST](https://ru.wikipedia.org/wiki/%D0%90%D0%B1%D1%81%D1%82%D1%80%D0%B0%D0%BA%D1%82%D0%BD%D0%BE%D0%B5_%D1%81%D0%B8%D0%BD%D1%82%D0%B0%D0%BA%D1%81%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%BE%D0%B5_%D0%B4%D0%B5%D1%80%D0%B5%D0%B2%D0%BE) - - | - [Примеры макросов на основе ExUnit, фреймворка для тестирования](https://github.com/elixir-lang/elixir/tree/main/lib/ex_unit/lib/ex_unit) - - | - [Веб фреймворк, в котором тоже много используется метапрограммирование](https://github.com/phoenixframework/phoenix) diff --git a/modules/60-macros/50-macros-ast/en/data.yml b/modules/60-macros/50-macros-ast/en/data.yml index 0edc9ca..866d6fe 100644 --- a/modules/60-macros/50-macros-ast/en/data.yml +++ b/modules/60-macros/50-macros-ast/en/data.yml @@ -1,2 +1,3 @@ +--- name: AST and final thoughts tips: [] diff --git a/modules/60-macros/50-macros-ast/ru/data.yml b/modules/60-macros/50-macros-ast/ru/data.yml index 0630c2a..b724167 100644 --- a/modules/60-macros/50-macros-ast/ru/data.yml +++ b/modules/60-macros/50-macros-ast/ru/data.yml @@ -1,3 +1,4 @@ +--- name: AST и подведение итогов tips: - | diff --git a/modules/65-extra/10-dates-and-times/description.en.yml b/modules/65-extra/10-dates-and-times/description.en.yml deleted file mode 100644 index d537ad2..0000000 --- a/modules/65-extra/10-dates-and-times/description.en.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- -name: Date and time -theory: | - - 🚧 In development - -instructions: | - - 🚧 In development - -tips: [] diff --git a/modules/65-extra/10-dates-and-times/description.ru.yml b/modules/65-extra/10-dates-and-times/description.ru.yml deleted file mode 100644 index 6f519f3..0000000 --- a/modules/65-extra/10-dates-and-times/description.ru.yml +++ /dev/null @@ -1,136 +0,0 @@ ---- -name: Даты и время -theory: | - - В Эликсире есть несколько модулей для работы с датами и временем, рассмотрим их. - - Первым в списке модуль `Time`, исходя из его названия, модуль предоставляет структуру данных и функции для работы со временем: - - ```elixir - current_time = Time.utc_now() - # => ~T[13:40:18.506292] - - current_time.hour # => 13 - current_time.minute # => 40 - current_time.second # => 18 - current_time.calendar # => Calendar.ISO - - current_time.day - # => ** (KeyError) key :day not found in: ~T[13:40:18.506292] - - Time.add(current_time, 20, :minute) - # => ~T[14:00:18.506292] - Time.add(current_time, 2, :hour) - # => ~T[15:40:18.506292] - ``` - - Второй модуль `Date`, он работает только с датами, без информации о времени: - - ```elixir - current_date = Date.utc_today() - # => ~D[2023-11-18] - - current_date.day # => 18 - current_date.month # => 11 - current_date.year # => 2023 - - Date.add(current_date, 5) - # => ~D[2023-11-23] - Date.add(current_date, -2) - # => ~D[2023-11-16] - - Date.beginning_of_week(current_date) - # => ~D[2023-11-13] - Date.end_of_week(current_date) - # => ~D[2023-11-19] - - Date.leap_year?(current_date) - # => false - ``` - - Третий модуль называется `NaiveDateTime`, он уже поддерживает и дату и время, однако, этот модуль не поддерживает часовые пояса: - - ```elixir - current = NaiveDateTime.utc_now() - # => ~N[2023-11-18 15:08:23.108947] - - NaiveDateTime.add(current, 5, :second) - # => ~N[2023-11-18 15:08:28.108947] - NaiveDateTime.add(current, -30, :millisecond) - # => ~N[2023-11-18 15:08:23.078947] - NaiveDateTime.add(current, 5, :day) - # => ~N[2023-11-23 15:08:23.108947] - - NaiveDateTime.end_of_day(current) - # => ~N[2023-11-18 23:59:59.999999] - NaiveDateTime.beginning_of_day(current) - # => ~N[2023-11-18 00:00:00.000000] - ``` - - Четвертый модуль `DateTime` не имеет такого недостатка, как `NaiveDateTime`, то есть `DateTime` поддерживает часовые пояса, но за этим нужно следить. Многие функции из этого модуля требуют базу данных часовых поясов. По умолчанию они используют базу данных, которую возвращает функция `Calendar.get_time_zone_database`, а эта база поддерживает только пояс `“Etc/UTC”`. Если нужно менять часовые пояса, советую присмотреться к библиотеке [tzdata](https://github.com/lau/tzdata). Теперь изучим `DateTime`: - - ```elixir - current = DateTime.utc_now() - # => ~U[2023-11-18 15:16:12.046038Z] - future = DateTime.add(current, 2, :day) - # => ~U[2023-11-20 15:16:12.046038Z] - - DateTime.compare(current, future) # => :lt - DateTime.diff(current, future) # => -172800 - - DateTime.from_naive!(NaiveDateTime.utc_now(), "Etc/UTC") - # => ~U[2023-11-18 15:18:54.394424Z] - - DateTime.to_date(current) - # => ~D[2023-11-18] - ``` - - Как было видно из примеров, время и даты можно задавать через строковые метки (сигили), для `Time` - `~T`, для `Date` - `~D`, для `NaiveDateTime` - `~N`, для `DateTime` - `~U` соответственно: - - ```elixir - ~T[13:40:18.506292] - # => ~T[13:40:18.506292] - - ~D[2023-11-18] - # => ~D[2023-11-18] - ``` - -instructions: | - - Создайте функцию `shift_days`, которая принимает структуры `Time`, `DateTime`, `NaiveDateTime`, `Date` и смещает ее на количество переданных дней. Дни могут быть отрицательным числом: - - ```elixir - naive_time = NaiveDateTime.utc_now() - # => ~N[2023-11-17 18:24:21.345116] - - - Solution.shift_days(naive_time, -2) - # => ~N[2023-11-15 18:24:21.345116] - Solution.shift_days(naive_time, 1) - # => ~N[2023-11-18 18:24:21.345116] - Solution.shift_days(naive_time, 0) - # => ~N[2023-11-17 18:24:21.345116] - - date = Date.utc_today() - # => ~D[2023-11-17] - Solution.shift_days(date, -2) - # => ~D[2023-11-15] - Solution.shift_days(date, 1) - # => ~D[2023-11-18] - Solution.shift_days(date, 0) - # => ~D[2023-11-17] - ``` - -tips: - - | - [Официальная документация Date](https://hexdocs.pm/elixir/Date.html) - - | - [Официальная документация DateTime](https://hexdocs.pm/elixir/DateTime.html) - - | - [Официальная документация NaiveDateTime](https://hexdocs.pm/elixir/NaiveDateTime.html) - - | - [Официальная документация Time](https://hexdocs.pm/elixir/Time.html) - - | - [Про базу знаний о часовых поясах tzdata](https://habr.com/ru/articles/130401/) - - | - [Библиотека tzdata для Elixir](https://github.com/lau/tzdata) diff --git a/modules/65-extra/10-dates-and-times/en/data.yml b/modules/65-extra/10-dates-and-times/en/data.yml index 6a2e8fd..ff02eda 100644 --- a/modules/65-extra/10-dates-and-times/en/data.yml +++ b/modules/65-extra/10-dates-and-times/en/data.yml @@ -1,2 +1,3 @@ +--- name: Date and time tips: [] diff --git a/modules/65-extra/10-dates-and-times/ru/data.yml b/modules/65-extra/10-dates-and-times/ru/data.yml index 9c81675..1a2f5f3 100644 --- a/modules/65-extra/10-dates-and-times/ru/data.yml +++ b/modules/65-extra/10-dates-and-times/ru/data.yml @@ -1,3 +1,4 @@ +--- name: Даты и время tips: - |