Livebook для Elixir: То, что заказывали разработчики

Хотя изначально Livebook задумывался как инструмент для исследования данных (подобно Jupyter для Python), он заслуженно стал сенсацией в сообществе Elixir.

Было фантастически интересно наблюдать за тем, как команды используют Livebook в самых разных случаях. Мы видели, как Livebook используется для:

  • Создания интерактивной документации для библиотек.
  • Создание материалов и руководств по введению в должность.
  • Аудит и изучение потенциальных зависимостей в вашем приложении.

Livebooks также использовались в качестве интерфейса REPL по умолчанию для разработки проектов.

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

Но сначала: Что такое Livebook для Elixir?

Livebook — это улучшенные файлы разметки, в которые вы можете добавлять секции произвольного исполняемого кода Elixir. Они вдохновлены похожими блокнотами для других языков (например, Jupyter для Python), но Livebook используют LiveView и другие преимущества BEAM, поэтому они еще лучше.

Файлы Livebook имеют собственное расширение .livemd, и (что несколько смущает) мы создаем и запускаем эти файлы разметки Livebook с помощью приложения Phoenix, которое также называется Livebook.

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

Предполагается, что вы установите репозиторий Livebook локально и запустите сервер Livebook с того места на вашей машине, где есть Livebook (файлы).

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

Теперь давайте рассмотрим библиотеку для документирования.

Livebook Docs: Единый источник правды в Elixir

Наша библиотека будет принимать различные входные данные и оценивать их в соответствии со следующей диаграммой Чонка:

Она будет выводить рейтинг chonk соответственно. Сначала создадим библиотеку.

mix new chonk_o_meter && cd chonk_o_meter
Войдите в полноэкранный режим Выйдите из полноэкранного режима

Добавим ex_doc к нашим deps в mix.exs:

defp deps do
  [
    {:ex_doc, ">= 0.0.0", runtime: false, only: [:docs, :dev]},
  ]
end
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь скачайте изображение графика chonk, приведенное выше, и поместите его в корень библиотеки в каталог images, чтобы мы могли ссылаться на него в нашем README. В README.md поместим заголовок и краткое объяснение того, для чего предназначена библиотека:

# Chonk 'O' Meter

Chonk O Meter is a state-of-the-art size estimator. It will rate the size of anything according to the following chart:

![alt chart showing cats of various sizes](./images/chonk.jpg)
Вход в полноэкранный режим Выход из полноэкранного режима

Пока все хорошо. Теперь мы откроем модуль и напишем наш moduledoc. Но вот в чем дело: на самом деле мы хотим просто скопировать то, что уже написали для README.

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

Хорошо повторять одну и ту же информацию в разных форматах, потому что разные люди будут приходить к библиотеке разными путями.

Некоторые могут сначала увидеть репозиторий (и, следовательно, README), в то время как другие могут сначала увидеть библиотеку на Hex — и поэтому увидят только moduledoc.

Вы можете подумать, что это достаточно просто — скопировать и вставить, но когда мы добавим Livebook, нам придется обновлять документацию в трех местах, когда что-то изменится!

Вместо этого давайте будем немного умнее. Мы выделим часть README и будем читать этот раздел в moduledoc во время компиляции. Поместим введение в библиотеку между двумя комментариями в README:

<!-- README START -->

.... Library introduction here.

<!-- README END -->
Войти в полноэкранный режим Выйти из полноэкранного режима

Теперь мы можем написать введение, как если бы это был moduledoc, между этими двумя комментариями, например, так:

<!-- README START -->

Chonk O Meter is a state-of-the-art size estimator. It will rate the size of anything according to the following chart:

![alt chart showing cats of various sizes](./images/chonk.jpg)

<!-- README END -->
Войти в полноэкранный режим Выйти из полноэкранного режима

В основном модуле ChonkOMeter мы можем сделать следующее:

defmodule ChonkOMeter do
  @moduledoc File.read!(Path.expand("./README.md"))
             |> String.split("<!-- README START -->")
             |> Enum.at(1)
             |> String.split("<!-- README END -->")
             |> List.first()
end
Войти в полноэкранный режим Выйти из полноэкранного режима

Это извлечет руководство, зажатое между комментариями в формате markdown, и установит его в качестве moduledoc. Теперь нам нужно только изменить README, чтобы обновить оба документа!

Мы можем сгенерировать документацию и просмотреть ее локально, чтобы убедиться, что все работает так, как ожидалось. Если вы запустите mix docs, появится папка doc с файлом index.html. Мы можем открыть doc/index.html, чтобы просмотреть нашу документацию в браузере.

Добавление изображения в Moduledoc

Если вы перейдете к документации модуля, вы заметите, что изображение отсутствует. Hex позволяет вам указывать на активы в документации, если они включены в каталог doc (созданный при создании документации командой mix docs).

Обычно эта команда перезаписывает всю папку doc. Поэтому, чтобы убедиться, что наши картинки всегда копируются туда, мы можем использовать псевдоним в нашем файле mix.exs. Мы превратим команду mix docs в команду, которая запускает mix docs и затем копирует все изображения из каталога /images в каталог doc/images.

defmodule ChonkOMeter.MixProject do
  use Mix.Project

  def project do
    [
      ...
      aliases: aliases(),
      ...
    ]
  end

  defp aliases() do
    [docs: ["docs", &copy_pictures/1]]
  end

  defp copy_pictures(_) do
    File.cp_r(Path.expand("./images/"), Path.expand("./doc/images/"))
  end
end
Вход в полноэкранный режим Выход из полноэкранного режима

Если теперь вы запустите mix docs, вы увидите, что каталог images/ будет скопирован в папку doc. Откройте файл doc/index.html и вы должны увидеть, что диаграмма chonk появилась!

Доктесты в Elixir’s Livebook

Важно отметить, что, написав наш moduledoc таким образом, мы не теряем ни одной из обычных возможностей, которые дают нам ex_docs.

Все, что вы обычно можете сделать в доктесте, вы можете сделать и здесь. Чтобы продемонстрировать это, давайте добавим doctest в наш README. Сначала нам понадобится функция для тестирования:

defmodule ChonkOMeter do
  @moduledoc File.read!(Path.expand("./README.md"))
             |> String.split("<!-- README START -->")
             |> Enum.at(1)
             |> String.split("<!-- README END -->")

  @doc """
  Returns the Chonk rating for a given number of story points.
  """
  def story_points(points) when is_integer(points) and points >= 0 and points < 3 do
    "A Fine Boi"
  end

  def story_points(points) when is_integer(points) and points >= 3 and points < 5 do
    "He Chomnk"
  end

  def story_points(points) when is_integer(points) and points >= 5 and points < 8 do
    "A Heckin' Chonker"
  end

  def story_points(points) when is_integer(points) and points >= 8 and points < 10 do
    "H E F T Y C H O N K"
  end

  def story_points(points) when is_integer(points) and points >= 10 and points < 15 do
    "Mega Chonk"
  end

  def story_points(points) when is_integer(points) and points >= 15 do
    "Oh Lawd He Comin'"
  end
end
Вход в полноэкранный режим Выйти из полноэкранного режима

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

defmodule ChonkOMeterTest do
  use ExUnit.Case
  doctest ChonkOMeter
end
Вход в полноэкранный режим Выход из полноэкранного режима

Запустите тесты, чтобы убедиться, что их пока нет:

mix test
Войти в полноэкранный режим Выйти из полноэкранного режима

Наконец, в нашем README мы можем добавить обычный синтаксис doctest:

<!-- README START -->

Chonk O Meter is a state-of-the-art size estimator. It will rate the size of anything according to the following chart:

![alt chart showing cats of various sizes](./images/chonk.jpg)

For example:

    iex> ChonkOMeter.story_points(10)
    "Mega Chonk"

<!-- README END -->
Enter fullscreen mode Выйти из полноэкранного режима

Если вы запустите тесты, то заметите, что изменение в README не вызвало повторной компиляции, то есть приложение по-прежнему считает, что доктестов нет. Чтобы исправить это, нужно просто добавить атрибут модуля @external_resource в главный модуль. Это указывает mix на необходимость перекомпиляции при изменении README:

defmodule ChonkOMeter do
  @external_resource Path.expand("./README.md")
  # ^^ Add this line ^^
  @moduledoc File.read!(Path.expand("./README.md"))
             |> String.split("<!-- README START -->")
             |> Enum.at(1)
             |> String.split("<!-- README END -->")

  @doc """
  Returns the Chonk rating for a given number of story points.
  """
  def story_points(points) when is_integer(points) and points >= 0 and points < 3 do
    "A Fine Boi"
  end

  def story_points(points) when is_integer(points) and points >= 3 and points < 5 do
    "He Chomnk"
  end

  def story_points(points) when is_integer(points) and points >= 5 and points < 8 do
    "A Heckin' Chonker"
  end

  def story_points(points) when is_integer(points) and points >= 8 and points < 10 do
    "H E F T Y C H O N K"
  end

  def story_points(points) when is_integer(points) and points >= 10 and points < 15 do
    "Mega Chonk"
  end

  def story_points(points) when is_integer(points) and points >= 15 do
    "Oh Lawd He Comin'"
  end
end
Войти в полноэкранный режим Выйти из полноэкранного режима

Когда мы запустим наши тесты, это приведет к одному проходящему доктесту! Мы также можем добавить doctest в нашу функцию doc следующим образом:

...
  @doc """
  Returns the Chonk rating for a given number of story points.

      iex> ChonkOMeter.story_points(5)
      "A Heckin' Chonker"
  """
  def story_points(points) when is_integer(points) and points >= 0 and points < 3 do
    "A Fine Boi"
  end

  def story_points(points) when is_integer(points) and points >= 3 and points < 5 do
    "He Chomnk"
  end

  def story_points(points) when is_integer(points) and points >= 5 and points < 8 do
    "A Heckin' Chonker"
  end

  def story_points(points) when is_integer(points) and points >= 8 and points < 10 do
    "H E F T Y C H O N K"
  end

  def story_points(points) when is_integer(points) and points >= 10 and points < 15 do
    "Mega Chonk"
  end

  def story_points(points) when is_integer(points) and points >= 15 do
    "Oh Lawd He Comin'"
  end
...
Вход в полноэкранный режим Выйти из полноэкранного режима

Добавление Livebook в Elixir

Итак, давайте подведем итоги. Сейчас у нас есть README как источник истины для нашего moduledoc. Мы можем иметь доктесты, изображения и все обычные плюсы, которые разрешены в moduledoc, но нам не нужно повторяться и рисковать ошибками копирования/вставки.

Мы хотим сохранить ту же энергию для нашего Livebook, чтобы не повторяться вручную, но при этом иметь интерактивную игровую площадку для нашей библиотеки поверх обычных moduledoc.

Для этого мы можем сгенерировать нашу Livebook из нашего модуля. Чтобы помочь в этом, я написал библиотеку, которую мы можем включить, под названием livebook_helpers:

defp deps do
  [
    {:ex_doc, ">= 0.0.0", runtime: false, only: [:docs, :dev]},
    {:livebook_helpers, ">= 0.0.0", only: [:docs, :dev]},
  ]
end
Вход в полноэкранный режим Выход из полноэкранного режима

После того, как мы получили deps с помощью mix deps.get, запуск mix help показывает одну дополнительную задачу mix:

...
mix create_livebook_from_module # Creates a livebook from the docs in the given module.
...
Войти в полноэкранный режим Выйти из полноэкранного режима

Из документации видно, что мы запускаем задачу mix, предоставляя модуль и путь к Livebook. Давайте попробуем это сделать:

mix create_livebook_from_module ChonkOMeter "chonk_o_meter_introduction"
Войти в полноэкранный режим Выйти из полноэкранного режима

Вы должны увидеть успешный вывод, который содержит ссылку на сгенерированный Livebook! 🎉 Есть еще одна вещь, которую мы можем сделать, чтобы сделать наш рабочий процесс бесшовным. Давайте добавим create_livebook_from_module в конец команды mix docs.

defmodule ChonkOMeter.MixProject do
  use Mix.Project

  def project do
    [
      ...
      aliases: aliases(),
      ...
    ]
  end

  defp aliases() do
    [docs: ["docs", &copy_pictures/1, &create_livebook/1]]
  end

  defp copy_pictures(_) do
    File.cp_r(Path.expand("./images/"), Path.expand("./doc/images/"))
  end

  defp create_livebook(_) do
    Mix.Task.run("create_livebook_from_module", ["ChonkOMeter", "chonk_o_meter_introduction"])
  end
end
Вход в полноэкранный режим Выход из полноэкранного режима

Каждый раз, когда мы запускаем mix docs, мы будем копировать любые статические изображения, используемые в README, и генерировать Livebook из нашего основного модуля!

Запуск Livebook в Elixir

Пока все хорошо! У нас есть хороший конвейер для создания полезного Livebook, но теперь нам нужно подумать о запуске Livebook. Запустите приложение Livebook следующим образом:

livebook server
Войти в полноэкранный режим Выйти из полноэкранного режима

По умолчанию секции Elixir имеют доступ только к стандартной библиотеке Elixir и Erlang. Если мы запустим нашу сгенерированную библиотеку, а затем попытаемся запустить Elixir-ячейку, которая вызывает библиотеку, это приведет к ошибке, поскольку код библиотеки отсутствует. Чтобы решить эту проблему, у нас есть два варианта — Mix.install или Livebook runtime.

Добавить Mix.install в Livebook

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

Mix.install([:chonk_o_meter])
Войти в полноэкранный режим Выйти из полноэкранного режима

При вызове мы получим последнюю версию библиотеки из Hex. Она будет доступна для всех последующих ячеек Elixir, точно так же, как при запуске Mix.install внутри IEx REPL.

Вы также можете легко указать версию и предоставить живую документацию для любой версии данной библиотеки:

Mix.install([{chonk_o_meter: ">=0.0.1"}])
Войти в полноэкранный режим Выход из полноэкранного режима

LivebookHelpers может даже генерировать Livebook с Mix.install в начале, если мы предоставим deps задаче mix:

mix create_livebook_from_module ChonkOMeter "chonk_o_meter_introduction" "[:chonk_o_meter"]"
Вход в полноэкранный режим Выйти из полноэкранного режима

Это отлично работает для любой библиотеки, развертываемой в Hex. Однако вы столкнетесь с проблемами, если, например, вам нужен Livebook для ветки main. В этом случае вы можете поступить следующим образом:

Mix.install [{:chonk_o_meter, path: "./"}]
Вход в полноэкранный режим Выйти из полноэкранного режима

Это говорит mix искать локальную версию библиотеки по указанному пути. Это, конечно, делает некоторые предположения о том, где будет запущен Livebook, поэтому полезно уточнить это. Если вы поместите Livebook в корень репозитория и пользователь запустит сервер Livebook оттуда, то путь "./" будет работать.

Однако если вы не хотите полагаться на это, Livebook прикроет вас мощной функцией: временем выполнения!

Время выполнения Livebook

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

Существует три варианта времени выполнения:

  • Встроенный
  • Прикрепленный узел
  • Автономный микс

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

Давайте рассмотрим каждый вариант времени выполнения ниже.

Режим встраивания

Режим Embedded Mode позволяет нам запускать код блокнота в самом узле Livebook! Это действительно для особых случаев, когда нет возможности запустить отдельную среду выполнения Elixir — например, на встроенных устройствах.

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

Присоединенный узел

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

Пока приложение запускается с известным вам cookie и именем sname, вы можете передать их Livebook и подключиться. Это похоже на получение удаленной оболочки в запущенном приложении, но с более полнофункциональной средой редактирования текста.

Использование присоединенного узла дает вам полный контроль над тем, как запускается ваше приложение. Вы получаете гораздо больше контроля, чем при использовании автономного узла, и можете делать всевозможные вещи, например, устанавливать env vars и запускать другие службы (например, базу данных).

Присоединенный узел может быть особенно актуален для создания внутренней (живой!) документации для закрытых репозиториев на работе, но не актуален для нас и нашей библиотеки.

Mix Standalone

Наконец, автономная среда выполнения Mix позволяет вам указать на проект mix, который Livebook скомпилирует и запустит (аналогично запуску iex -S mix в терминале).

Mix standalone дает большое преимущество перед Mix.install, так как вы можете перекомпилировать его! Это полезно, если вы пишете Livebook с нуля; в этом случае вы, скорее всего, добавите что-то в библиотеку, которую потом захотите использовать в Livebook.

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

IEx.Helpers.recompile()
Войти в полноэкранный режим Выйти из полноэкранного режима

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

Livebook for Docs: Попробуйте сами в своем приложении Elixir

На этом мы завершаем знакомство с Livebook for docs. Вы можете посмотреть пример библиотеки chonk_o_meter здесь. Чтобы увидеть пример применения этих идей в реальной библиотеке, посмотрите также мою библиотеку data_schema.

В настоящее время GitHub не распознает расширение .livemd, так что если вы играете с Livebook, я бы посоветовал вам выкладывать его в публичные репозитории. Чем больше мы будем это делать, тем больше шансов, что GitHub будет разбирать файлы с хорошей подсветкой синтаксиса и рендерингом разметки.

Но пока этого не произошло, мы можем поставить волшебную строку в верхней части Livebook, чтобы заставить GitHub отображать его в виде уценки:

<!-- vim: syntax=markdown -->
Войти в полноэкранный режим Выйти из полноэкранного режима

Livebook, который генерирует Livebook helpers, будет включать эту строку для вас. Теперь вы обладаете всеми необходимыми знаниями и можете приступать к созданию живых документов!

Счастливого кодинга!

P.S. Если вы хотите читать посты Elixir Alchemy сразу после их выхода из печати, подпишитесь на нашу рассылку Elixir Alchemy и не пропустите ни одного поста!

Оцените статью
Procodings.ru
Добавить комментарий