Spec-Zone.ru › Elixir 1.18

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

В этой главе рассматриваются механизмы ввода/вывода, задачи, связанные с файловой системой, и такие модули, как 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 для операций ввода/вывода.

← Предыдущая страница Процессы
Следующая страница → alias, require, import и use

Скачать версию ePub

Создано с помощью ExDoc (v0.36.1) для языка программирования Elixir

© 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

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API