Эта глава – быстрое введение в механизмы ввода/вывода и задачи, связанные с файловой системой, а также модулями 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
.