Источник Ввод-вывод и файловая система
В этом разделе рассматриваются механизмы ввода-вывода, задачи, связанные с файловой системой, и соответствующие модули, такие как IO, File и Path. Система ввода-вывода предоставляет отличную возможность узнать некоторые философские аспекты и особенности Elixir и виртуальной машины Erlang.
Модуль IO
Модуль IO является основным механизмом в Elixir для чтения и записи в стандартный ввод/вывод (:stdio), стандартную ошибку (:stderr), файлы и другие устройства ввода-вывода. Использование модуля довольно простое:
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.binread/2 и IO.binwrite/2 из модуля IO:
Предупреждение о потенциальной потере данных
Следующий код открывает файл для записи. Если файл с указанным путем уже существует, его содержимое будет удалено.
iex> {:ok, file} = File.open("path/to/file/hello", [:write])
{:ok, #PID<0.47.0>}
iex> IO.binwrite(file, "world")
:ok
iex> File.close(file)
:ok
iex> File.read("path/to/file/hello")
{:ok, "world"}
Файл можно открыть с опцией :append, вместо :write, чтобы сохранить его содержимое. Также можно передать опцию :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("path/to/file/hello")
{:ok, "world"}
iex> File.read!("path/to/file/hello")
"world"
iex> File.read("path/to/file/unknown")
{:error, :enoent}
iex> File.read!("path/to/file/unknown")
** (File.Error) could not read file "path/to/file/unknown": no such file or directory
Обратите внимание, что версия с ! возвращает содержимое файла вместо кортежа, и если что-то пойдёт не так, функция вызывает ошибку.
Версия без ! предпочтительнее, когда вы хотите обработать разные результаты с помощью сопоставления шаблонов:
case File.read("path/to/file/hello") do
{:ok, body} -> # do something with the `body`
{:error, reason} -> # handle the error caused by `reason`
end
Однако, если вы ожидаете, что файл будет там, вариант с восклицательным знаком более полезен, так как он генерирует осмысленное сообщение об ошибке. Избегайте записи:
{:ok, body} = File.read("path/to/file/unknown")
поскольку в случае ошибки 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 прозрачно учитывает разные операционные системы. Наконец, помните, что Elixir автоматически преобразует косые черты (/) в обратные косые черты (\) в Windows при выполнении операций с файлами.
С этим мы рассмотрели основные модули, которые предоставляет Elixir для работы с вводом-выводом и взаимодействием с файловой системой. В следующем разделе мы немного заглянем под капот и узнаем, как реализована система ввода-вывода в виртуальной машине.
Процессы
Вы могли заметить, что File.open/2 возвращает кортеж, подобный {:ok, pid}:
iex> {:ok, file} = File.open("hello")
{:ok, #PID<0.47.0>}
Это происходит потому, что модуль IO фактически работает с процессами (см. предыдущий раздел). Поскольку файл — это процесс, когда вы записываете в закрытый файл, вы фактически отправляете сообщение завершенному процессу:
iex> File.close(file)
:ok
iex> IO.write(file, "is anybody out there")
** (ErlangError) Erlang error: :terminated:
* 1st argument: the device has terminated
(stdlib 5.0) io.erl:94: :io.put_chars(#PID<0.114.0>, "is anybody out there")
iex:4: (file)
Давайте подробнее рассмотрим, что происходит при запросе 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 ожидал какой-либо результат, которого мы не предоставили.
Моделируя устройства ввода-вывода с помощью процессов, виртуальная машина Erlang позволяет даже читать и записывать файлы на разных узлах. Прекрасно!
iodata и chardata
Во всех примерах выше мы использовали двоичные данные при записи в файлы. Однако большинство функций ввода-вывода в Elixir также принимают «iodata» или «chardata».
Одной из основных причин использования «iodata» и «chardata» является производительность. Например, представьте, что вам нужно приветствовать кого-то в вашем приложении:
name = "Mary"
IO.puts("Hello " <> name <> "!")
Поскольку строки в Elixir, как и большинство структур данных, являются неизменяемыми, в примере выше строка «Mary» будет скопирована в новую строку «Hello Mary!». Хотя это маловероятно, что это будет иметь значение для короткой строки, как в данном примере, копирование может быть довольно дорогостоящим для длинных строк! По этой причине функции ввода-вывода в Elixir позволяют передавать вместо этого список строк:
name = "Mary" IO.puts(["Hello ", name, "!"])
В примере выше копирования не происходит. Вместо этого мы создаем список, содержащий исходное имя. Такие списки называются либо «iodata», либо «chardata», и мы скоро узнаем точное различие между ними.
Эти списки очень полезны, поскольку могут упростить обработку строк в нескольких сценариях. Например, представьте, что у вас есть список значений, например, ["apple", "banana", "lemon"], которые вы хотите записать в файл, разделив их запятыми. Как это можно сделать?
Один из вариантов — использовать Enum.join/2 и преобразовать значения в строку:
iex> Enum.join(["apple", "banana", "lemon"], ",") "apple,banana,lemon"
Выше возвращается новая строка, в которой каждое значение копируется в новую строку. Однако, зная из этого раздела, что мы можем передать список строк функциям IO/File, мы можем сделать следующее:
iex> Enum.intersperse(["apple", "banana", "lemon"], ",") ["apple", ",", "banana", ",", "lemon"]
«iodata» и «chardata» содержат не только строки, но и произвольные вложенные списки строк:
iex> IO.puts(["apple", [",", "banana", [",", "lemon"]]])
«iodata» и «chardata» также могут содержать целые числа. Например, мы можем вывести наш список значений, разделённых запятыми, используя ?, в качестве разделителя, который является целым числом, представляющим запятую (44):
iex> IO.puts(["apple", ?,, "banana", ?,, "lemon"])
Разница между «iodata» и «chardata» — в том, что представляют собой эти целые числа. Для iodata целые числа представляют байты. Для chardata целые числа представляют коды Юникода. Для символов ASCII числовое представление совпадает с кодом, поэтому оно подходит для обоих классификаций. Однако, по умолчанию устройство ввода-вывода работает с chardata, что означает, что мы можем сделать:
iex> IO.puts([?O, ?l, ?á, ?\s, "Mary", ?!])
В целом, целые числа в списке могут представлять либо набор байтов, либо набор символов, и выбор зависит от кодировки устройства ввода-вывода. Если файл открыт без кодировки, файл ожидается в сыром виде, и функции в модуле IO, начинающиеся с bin*, должны быть использованы. Эти функции ожидают iodata в качестве аргумента, где целые числа в списке будут представлять байты.
С другой стороны, устройство ввода-вывода по умолчанию (:stdio) и файлы, открытые с кодировкой :utf8, работают с оставшимися функциями в модуле IO. Эти функции ожидают chardata в качестве аргумента, где целые числа представляют коды символов.
Хотя это тонкое различие, вам нужно беспокоиться об этом только в том случае, если вы намерены передавать списки, содержащие целые числа, в эти функции. Если вы передаёте двоичные данные или список двоичных данных, то нет двусмысленности.
Наконец, есть ещё один конструктор — charlist, о котором мы говорили в предыдущих разделах. Charlists — это особый случай chardata, где все значения являются целыми числами, представляющими коды Юникода. Они могут быть созданы с помощью синтаксического элемента ~c:
iex> ~c"hello" ~c"hello"
Charlists в основном встречаются при взаимодействии с Erlang, так как некоторые API Erlang используют charlist в качестве представления строк. По этой причине любой список, содержащий удобочитаемые символы ASCII, будет напечатан как charlist:
iex> [?a, ?b, ?c] ~c"abc"
Мы упаковали много информации в этот небольшой раздел, поэтому давайте разберём его.
iodata и chardata — списки бинарных данных и целых чисел. Эти бинарные данные и целые числа могут быть произвольно вложены внутри списков. Их цель — обеспечить гибкость и производительность при работе с устройствами ввода-вывода и файлами;
выбор между iodata и chardata зависит от кодировки устройства ввода-вывода. Если файл открыт без кодировки, файл ожидает iodata, и необходимо использовать функции в модуле
IO, начинающиеся сbin*. Стандартное устройство ввода-вывода (:stdio) и файлы, открытые с кодировкой:utf8, ожидают chardata и работают с оставшимися функциями в модулеIO;charlists — это особый случай chardata, где используется исключительно список целых чисел — кодов Юникода. Их можно создать с помощью сигила
~c. Списка целых чисел автоматически выводятся с использованием сигила~c, если все целые числа в списке представляют напечатаемые символы ASCII.
На этом заканчивается наше знакомство с устройствами ввода-вывода и функциональностью, связанной с вводом-выводом. Мы узнали о трех модулях Elixir — IO, File и Path — а также о том, как виртуальная машина использует процессы для базовых механизмов ввода-вывода и о том, как использовать chardata и iodata для операций ввода-вывода.
© 2012-2024 The Elixir Team
Licensed under the Apache License, Version 2.0.
https://hexdocs.pm/elixir/1.17.2/io-and-the-file-system.html