Источник Ввод/вывод и файловая система
В этой главе рассматриваются механизмы ввода/вывода, задачи, связанные с файловой системой, и такие модули, как IO, File и Path. Система ввода/вывода предоставляет отличную возможность познакомиться с некоторыми философскими аспектами и особенностями Elixir и Erlang VM.
Модуль 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 для работы с вводом/выводом и взаимодействием с файловой системой. В следующей секции мы немного заглянем под капот и узнаем, как система ввода/вывода реализована в VM.
Процессы
Вы могли заметить, что 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
...> 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 VM позволяет даже читать и записывать в файлы на разных узлах. Удобно!
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"
Это возвращает новую строку, копируя каждое значение в новую строку. Однако, зная из этой секции, что мы можем передавать список строк функциям ввода/вывода/файлов. Поэтому вместо этого мы можем сделать:
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 целые числа представляют кодовые точки Unicode. Для ASCII-символов представление байта совпадает с представлением кодовой точки, поэтому оно подходит для обоих классификаций. Однако, устройство ввода/вывода по умолчанию работает с chardata, что означает, что мы можем сделать:
iex> IO.puts([?O, ?l, ?á, ?\s, "Mary", ?!])
Список символов, например ~c"hello world", представляет собой список целых чисел и, следовательно, является chardata.
Мы вложили много в эту небольшую секцию, поэтому давайте разберём её:
iodata и chardata — это списки двоичных данных и целых чисел. Эти двоичные данные и целые числа могут быть произвольно вложены внутри списков. Их цель — гибкость и производительность при работе с устройствами ввода/вывода и файлами;
выбор между iodata и chardata зависит от кодировки устройства ввода/вывода. Если файл открыт без кодировки, файл ожидает iodata, и функции модуля
IO, начинающиеся сbin*, должны быть использованы. Устройство ввода/вывода по умолчанию (:stdio) и файлы, открытые с кодировкой:utf8, ожидают chardata и работают с оставшимися функциями модуляIO;
Это завершает наше знакомство с устройствами ввода/вывода и функциональностью, связанной с вводом/выводом. Мы изучили три модуля Elixir — IO, File и Path — а также то, как VM использует процессы для механизмов ввода/вывода на низком уровне, и как использовать chardata и iodata для операций ввода/вывода.
© 2012-2024 The Elixir Team
Licensed under the Apache License, Version 2.0.
https://hexdocs.pm/elixir/1.18.1/io-and-the-file-system.html