Говорим о новом

Эликсир как язык для бекенда микросервиса или веб приложения

Автор: Давыденков Михаил

Содержание

  • Что мы ждём от современного веб фреймворка и платформы для бекенда
  • Рельсы, феникс и конкуренты
  • С чего начать
  • Знакомимся с эликсиром, демки
  • Обзор phoenix приложения
  • Ссылочки

Ожидания от платформы / рантайма

  • Масштабируемость из коробки (при увеличении нагрузок вычисления могут быть реорганизованы без головной боли)
  • Равномерное распределение вычислений по процессорным ядрам
  • Прозрачность и простота написания асинхронного кода
  • Продуманная модель обработки ошибок (и восстановления после сбоев)
  • Достаточно высокий уровень языка, чтобы не отвлекаться на низкоуровневые вещи
  • Поддержка метапрограммирования и всякие синтаксические удобности

Ожидания от веб-фреймворка

  • Возможность работать с IO в неблокирующем стиле
  • RESTful по умолчанию
  • Прозрачный механизм сборки и управления ассетами
  • Удобная прослойка для работы с БД
  • Хороший тестовый фреймворк с возможностью запуска параллельных тестов
  • Наличие удобных хелперов (декораторы, локали, сервисы)
  • Нативная поддержка модных штук типа Websockets, SSE, HTTP2

Дружелюбность к разработчику

  • Удобные инструменты работы с платформой (REPL, билд тулы, таск раннеры, дебагеры и тд.)
  • Быстрая скорость разработки
  • Приятный синтаксис
  • Наличие отзывчивого комьюнити, third-party библиотек, хорошая документация
  • Возможности дебажить на стейджинг серверах

К сожалению не все языки и фреймворки соответствуют современным требованиям

Иногда бывает так:

arch1

Или так:

arch1

Рельсы, феникс и конкуренты

  • node.js
  • Go
  • ASP.NET
  • JAVA фреймворки (Scala play)
  • Разное из мира ФП (uncommon web на Clojure, Haskell, OCaml)
  • Python, Perl, PHP фреймворки

С чего начать

Ставим зависимости


                $ sudo aptitude install -y \
                build-essential \
                libncurses5-dev \
                openssl \
                libssl-dev \
                fop \
                xsltproc \
                unixodbc-dev \
                systemtap-sdt-dev \
                autoconf 

                To enable GUI Toolkit:

                $ sudo aptitude install -y \
                libwxbase2.8 \
                libwxgtk2.8-dev \
                libqt4-opengl-dev
              

Устанавливаем менеджер версий эрланга - KERL


                curl -O https://raw.githubusercontent.com/yrashk/kerl/master/kerl
                chmod a+x kerl && sudo mv /usr/bin

                KERL_CONFIGURE_OPTIONS=" \
                --with-dynamic-trace=dtrace \
                --enable-dynamic-ssl-lib \
                --enable-dirty-schedulers \
                --enable-smp-support \
                --enable-sctp \
                --enable-threads \
                --enable-kernel-poll \
                --enable-shared-zlib \
                --disable-silent-rules \
                --disable-debug \
                --enable-hipe \
                --enable-native-libs \
                " \
                kerl build 18.2.1 18.2.1
                kerl install 18.2.1 18.2.1
            

Устанавливаем менеджер версий эликсира - KIEX


                \curl -sSL https://raw.githubusercontent.com/taylor/kiex/master/install | bash -s
                kiex install 1.2.3
                kiex default 1.2.3
            

Добавляем в .bash_profile или .zprofile


            export PATH=$PATH:$HOME/.kerl/18.2.1/bin
            export PATH=$PATH:$HOME/.kiex/bin
            source "$HOME/.kiex/elixirs/elixir-1.2.3.env"

            должно работать:
            mix new project_name
            

Далее ставим феникс и можно работать


            mix archive.install https://github.com/phoenixframework/archives/raw/master/phoenix_new.ez

            должно работать:
            mix phoenix.new project_name
            

Elixir overview

Главная фишка эликсира - это BEAM, виртуальная машина эрланга. Посмотрим чем может удивить эрланг!

Простейшие функции


            hello_func = fn ->
              IO.puts "hello"
            end
            

Порождение процесса


            spawn(hello_func)
            

Порождение процесса, ожидающего сообщениe


            hello_func = fn ->
              IO.puts "hello"
              receive do
                :goodbye ->
                  IO.puts "goodbye"
                _ ->
                  IO.puts "unknown message"
              end
            end

            spawn(hello_func)
            

Процесс завершается с exit(normal) и второй раз не отреагирует на наше сообщение

Функция в модуле (c хвостовой рекурсией)


            defmodule Hello do
              def hello_func do
                IO.puts "hello"
                receive do
                  :goodbye ->
                    IO.puts "goodbye"
                    hello_func
                  _ ->
                    IO.puts "unknown message"
                    hello_func
                end
              end
            end

            pid = spawn(Hello, :hello_func, [])
            send(pid, :goodbye)
            send(pid, :foobar)
            

Функция в модуле передающая стейт между вызовами (c хвостовой рекурсией)


            defmodule Hello do
              def hello_func(counter) do
                IO.puts "hello"
                receive do
                  :goodbye ->
                    IO.puts "You've said goodbye #{counter} times"
                    new_counter = counter + 1
                    hello_func(new_counter)
                  _ ->
                    IO.puts "unknown message"
                    hello_func(counter)
                end
              end
            end

            pid = spawn(Hello, :hello_func, [])
            send(pid, :goodbye)
            send(pid, :foobar)
            

Интроспекция в эрланге / эликсире


            :observer.start
            

А вообще тема серьёзная и требует изучения (есть разные интсрументы как в эрланге, так и в эликсире)

Можно почитать тут и тут для ознакомления

Составление пифагоровых троек через генераторы списков (List Comprehensions)


              defmodule Triple do
                def pythagorean(n) when n > 0 do
                  for a <- 1..n,
                    b <- 1..n,
                    c <- 1..n,
                    a + b + c <= n,
                    a*a + b*b == c*c,
                    do: {a, b, c}
                end
              end

              Triple.pythagorean(48)
            

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

Удобности для асинхронного кода (промисы)


            task = Task.async(fn -> 1 + 2 + 3 end)
            res  = 4 + 5
            res + Task.await(task)
            

Распределённый вариант


            task = Task.Supervisor.async {CustomSupervisor, :"foo@computer-name"}, fn ->
              {:ok, node()}
            end
            

Пример скрипта на async await


              defmodule WebsitePipeline do
                #  def map_titles(sites) do
                #    sites
                #    |> Enum.map(fn(url) ->
                #      url |> get_body |> extract_title
                #    end)
                #  end

                def map_titles(sites) do
                  sites
                  |> Enum.map(fn(url) ->
                    Task.async(fn -> url |> get_body |> extract_title end)
                  end)
                  |> Enum.map(&Task.await/1)
                end

                defp get_body(url) do
                  IO.inspect url
                  HTTPotion.get(url).body
                end

                defp extract_title(html) do
                  title_pattern = ~r"([^<]*)"
                  Regex.run(title_pattern, html) |> Enum.at(1)
                end
              end

              defmodule WebsitePipelineTest do
                use ExUnit.Case

                test "Mapping a pipeline of websites" do
                  sites = ["http://example.org",
                           "http://slashdot.org",
                           "http://elixir-lang.org",
                           "http://www.erlang.org"]
                  expected_titles = ["Example Domain",
                                     "Slashdot: News for nerds, stuff that matters",
                                     "Elixir",
                                     "Erlang Programming Language"]
                  assert expected_titles == WebsitePipeline.map_titles(sites)
                end
              end
            

Управление in-memory состоянием (простая абстракция вокруг стейта процесса - Agent)


            {:ok, agent} = Agent.start_link fn -> [] end
            Agent.update(agent, fn list -> ["eggs"|list] end)
            Agent.get(agent, fn list -> list end)
            Agent.stop(agent)
            

Умеет get_and_update за одну операцию

Управление in-memory состоянием (GenServers)

Если что-то посложнее и похитрее, то правильнее использовать GenServer

Любимая программа Джо Армстронга


            # shell 1
            iex --name "foo@192.168.1.69"
            :erlang.set_cookie(node(), :foobar)
            :erlang.register(:shell, self())

            returned_value = receive do
              {:become, some_fn} ->
                some_fn.()
            end

            #shell 2
            iex --name "bar@192.168.1.60"
            erlang.set_cookie(node(), :foobar)

            echo_server = fn ->
              receive do
                {from, value} ->
                  send(from, value)
              end
            end

            foo = {:shell, :"foo@192.168.1.69"}
            send(foo, {:become, echo_server})
            send(foo, {self, "hey there"})
            flush()
            

Протоколы - вариант реализации полиморфизма в эликсире


            defprotocol Odd do
              @doc "Returns true if data is considered odd"

              def odd?(data)
            end

            defimpl Odd, for: Integer do
              require Integer

              def odd?(data) do
                data
                |> Integer.is_odd
              end
            end

            defimpl Odd, for: Float do
              def odd?(data) do
                Odd.odd?(round(Float.floor(data)))
              end
            end

            defimpl Odd, for: List do
              def odd?(data) do
                Odd.odd?(Enum.count(data))
              end
            end

            Odd.odd?(2.1)
            Odd.odd?([1])
            Odd.odd?(%Animal{})
            

Активно используется в phoenix для сериализации-десериализации

Если необходимо можно использовать эрланговские библиотеки без особых проблем

Пример простого сервиса c использованием пула воркеров на эрланге (poolboy)

А ещё в elixir довольно хорошие возможности по метапрограммированию, созданию макросов и написанию DSL

Можно почитать в оффициальных доках

Phoenix overview


Рассмотрим на реальном примере - Phoenix-trello

что хотелось бы отметить:

  • асинхронные тесты (есть даже интеграционные на хромдрайвере), фабрики, теги для тестов
  • миграции и сиды так знакомые rails разработчикам
  • brunch.io заменён на webpack
  • есть библиотека для авторизации через JWT, которая используется для аутентификации каналов, тсп сокетов и ендпоинтов
  • есть довольно приятный статический анализатор стиля кода аля рубокоп (mix credo)
  • довольно оправданный кейс для использования веб сокетов

Другой пример - Hex Web, API сервер для сайта package менеджера Hex.pm

Ссылочки

THE END