Ввод/вывод и файловая система

Эта глава – быстрое введение в механизмы ввода/вывода и задачи, связанные с файловой системой, а также модулями IO, File и Path.

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

Модуль IO

Модуль IO – основной механизм Эликсира для работы со стандартным вводов/выводом (:stdio), стандартным выводом ошибок (:stderr), файлами, и другими устройствами IO. Его использование простое и очевидное:

iex> IO.puts "hello world"
hello world
:ok

iex> IO.gets "yes or no? "
yes or no? yes
"yes\n"

По умолчанию, функции из модуля IO читают стандартный ввод и пишут в стандартный вывод. Мы можем изменить это, передав, например, :stderr в качестве аргумента (чтобы осуществить запись в стандартное устройство вывода ошибок):

iex> IO.puts :stderr, "hello world"
hello world
:ok

Модуль File

Модуль File содержит функции, которые позволяют открывать файлы как IO-устройства. По умолчанию, файлы открываются в бинарном режиме, что обязывает разработчиков использовать специальные функции IO.binread/2 и IO.binwrite/2 из модуля IO:

iex> {:ok, file} = File.open "hello", [:write]
{:ok, #PID<0.47.0>}

iex> IO.binwrite file, "world"
:ok

iex> File.close file
:ok

iex> File.read "hello"
{:ok, "world"}

Файл может быть также открыт с указанием кодировки :utf8, в этом случае модуль File будет интерпретировать байты, прочитанные из файла, как байты кодировки UTF-8.

Кроме функций для открытия, чтения и записи файлов, модуль File имеет много функций для работы с файловой системой. Эти функции названы соответственно их Unix-эквивалентам. Например, функцию File.rm/1 можно использовать для удаления файла, функцию File.mkdir/1 для создания директорий, а функцию File.mkdir_p/1 для создания директорий и последовательности её предков. Есть также функции File.cp_r/2 и File.rm_rf/1 для копирования директорий со всем содержимым и рекурсивного удаления директории и всех её файлов.

Вы также можете обнаружить, что функции в модуле File представлены в двух вариантах: «обычный» вариант и принудительный вариант, оканчивающийся восклицательным знаком(!). Например, когда мы читаем файл "hello" в примере выше, мы используем функцию File.read/1. Мы можем также использовать функциюFile.read!/1:

iex> File.read "hello"
{:ok, "world"}

iex> File.read! "hello"
"world"

iex> File.read "unknown"
{:error, :enoent}

iex> File.read! "unknown"
** (File.Error) could not read file "unknown": no such file or directory

Обратите внимание, что версия с ! возвращает содержимое файла, а не кортеж, и если что-то идёт не так, выбрасывает ошибку.

Версия без ! предпочтительна, если вы хотите обработать разные варианты вывода, используя сравнение с образцом:

case File.read(file) do
  {:ok, body}      -> # сделать что-нибудь с `body`
  {:error, reason} -> # обработать ошибку с причиной `reason`
end

Однако, если вы уверены, что файл существует, принудительный вариант несёт больше пользы, т. к. выдаёт понятное сообщение об ошибке. Не пишите так:

{:ok, body} = File.read(file)

Ведь в случае ошибки функция File.read/1 вернёт кортеж {:error, reason}, и это не будет соответствовать образцу, сравнение не пройдёт. Вы также получите желаемый результат (выброшенную ошибку), но она будет связана с отсутствием подходящего шаблона (что не даст нам понять, в чём же на самом деле проблема с файлом).

Если не собираетесь обрабатывать ошибки, используйте функцию File.read!/1.

Модуль Path

Большинство функций в модуле File принимают пути в качестве аргументов. Модуль Path предоставляет возможности для работы с такими путями:

iex> Path.join("foo", "bar")
"foo/bar"

iex> Path.expand("~/hello")
"/Users/jose/hello"

Использование функций из модуля Path вместо прямых манипуляций со строками является предпочтительным, т. к. модуль Path заботится о различиях операционных систем. Наконец, помните, что Эликсир автоматически конвертирует косые черты (/) в обратные косые черты (\) в Виндоус при исполнении файловых операций.

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

Процессы и лидеры групп

Вы могли заметить, что функция File.open/2 возвращает кортеж вроде {:ok, pid}:

iex> {:ok, file} = File.open "hello", [:write]
{:ok, #PID<0.47.0>}

Это происходит, потому что модуль IO на самом деле работает с процессами (смотрите главу 11). Когда вы пишете IO.write(pid, binary), модуль IO отправляет сообщение процессу с идентификатором pid с желаемой операцией. Давайте посмотрим, что происходит, если мы используем наш собственный процесс:

iex> pid = spawn fn ->
...>  receive do: (msg -> IO.inspect msg)
...> end
#PID<0.57.0>

iex> IO.write(pid, "hello")
{:io_request, #PID<0.41.0>, #Reference<0.0.8.91>,
 {:put_chars, :unicode, "hello"}}
** (ErlangError) erlang error: :terminated

После IO.write/2 мы можем увидеть запрос, отправленный модулем IO (кортеж из четырех элементов). Сразу после мы видим ошибку, т. к. модуль IO ожидает некоторый результат, который мы не предоставляем.

Модуль StringIO – реализация сообщений для устройств ввода-вывода поверх строк:

iex> {:ok, pid} = StringIO.open("hello")
{:ok, #PID<0.43.0>}

iex> IO.read(pid, 2)
"he"

При моделировании устройств ввода-вывода с процессами, виртуальная машина Эрланга позволяет разным узлам одной сети обмениваться файловыми процессами и читать/записывать файлы между узлами. Среди всех устройств ввода-вывода есть одно особое для каждого процесса – лидер группы.

Когда вы пишете в :stdio, вы на самом деле отправляете сообщение лидеру группы, который пишет в файловый дескриптор для стандартного вывода:

iex> IO.puts :stdio, "hello"
hello
:ok

iex> IO.puts Process.group_leader, "hello"
hello
:ok

Лидера группы можно задать для процесса и использовать в разных ситуациях. Например, при исполнении кода на удалённом терминале, он гарантирует, что сообщение на удалённом узле будет переправлено и напечатано на терминале, который обрабатывает запрос.

Аргументы iodata и chardata

Во всех примерах выше, мы использовали бинарные последовательности для записи файлов. В главе «Двоичные данные, строки и списки символов» мы упоминали, как строки формируются из байтов, в то время как списки символов – это списки с кодами Юникода.

Функции в модулях IO и File также принимают списки в качестве аргументов. Кроме того, они позволяют смешивать списки из списков, целых чисел и бинарных последовательностей:

iex> IO.puts 'hello world'
hello world
:ok

iex> IO.puts ['hello', ?\s, "world"]
hello world
:ok

Однако, использовать списки в операциях ввода/вывода следует осторожно. Список может представлять набор байтов или набор символов, и что из них использовать – зависит от кодировки устройства ввода/вывода. Если файл открыт без кодировки, ожидается, что файл находится в «сыром» режиме и с ним нужно использовать функции из модуля IO, которые начинаются с bin*. Эти функции принимают в качестве аргумента iodata. Например, они ожидают список чисел, представляющих байты или бинарные последовательности на вход.

С другой стороны, :stdio и файлы, открытые с кодировкой :utf8, работают с остальными функциями модуля IO. Эти функции принимают в качестве аргумента char_data, списки символов или строки.

Хотя это существенная разница, вам не нужно беспокоиться об этих деталях, если вы посылаете списки в эти функции. Бинарные последовательности уже представлены байтами и их представление в памяти всегда «сырое».

На этом наш обзор ввода/вывода и связанных с ними функций заканчивается. Мы ознакомились с модулями Эликсира: IO, File, Path и StringIO. Также мы узнали как виртуальная машина использует процессы для механизмов ввода/вывода, и как использовать chardata и iodata.

© 2020 / Россия Любые мысли и вопросы пишите на elixir@wunsh.ru.