12 入出力 - IO

この章はElixirの入出力機構,および,IOFileあるいはPathなどの入出力機構へ関連するモジュールについての紹介をします.

This chapter is a quick introduction to input/output mechanisms in Elixir and related modules, like IO, File and Path.

この章の(入出力の)概要はGetting Startedの前の版にもすでにありました.ですが,入出力システムはElixirとそのVMについていくつかの方針とおもしろさへ光を当てるすばらしい機会を用意していることに気がつきました.

We had originally sketched this chapter to come much earlier in the getting started guide. However, we noticed the IO system provides a great opportunity to shed some light on some philosophies and curiosities of Elixir and the VM.

12.1 入出力モジュール - The IO module

ElixirのIOモジュールは標準入出力(:stdio),標準エラー(:stderr),ファイル,あるいは他の入出力デバイスを読み書きするための基本となる部分です.モジュールの使い方はこのようになります:

The IO module in Elixir is the main mechanism for reading and writing to the standard io (:stdio), standard error (:stderr), files and other IO devices. Usage of the module is pretty straight-forward:

iex> IO.puts "hello world"
"hello world"
:ok
iex> IO.gets "yes or no? "
yes or no? yes
"yes\n"

デフォルトでは入出力モジュールの関数は標準入力と標準出力を利用します.引数として:stderrを渡すことで標準エラーデバイスへ出力することができます:

By default, the functions in the IO module use the standard input and output. We can pass the :stderr as argument to write to the standard error device:

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

12.2 ファイルモジュール - The File module

Fileモジュールはファイルを入出力デバイスとして開くことのできる関数を持っています.デフォルトではファイルはバイナリモードで開かれます,必要があればIOモジュールのIO.binread/2IO.binwrite/2を使うこともできます:

The File module contains functions that allows us to open files as IO devices. By default, files are opened in binary mode, which requires developers to use the specific IO.binread/2 and IO.binwrite/2 functions from the IO module:

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エンコーディングで開くこともでき,(訳注:エンコーディングの指定は)IOモジュールの他の関数でも使えます.

A file can also be opened with :utf8 encoding which allows the remaining functions in the IO module to be used:

iex> {:ok, file} = File.open "another", [:write, :utf8]
{:ok, #PID<0.48.0>}

一方,ファイルを開いたり,読み書きする関数については,Fileモジュールがファイルシステム上で動く関数をたくさん持っています.例えばFile.rm/1はファイルを削除するのに使え,File.mkdir/1はディレクトリを作り,File.mkdir_p/1は必ず親ディレクトリがあるようにディレクトリを(場合によっては複数)作り,File.cp_r/2File.rm_rf/2はファイルとディレクトリのコピーと削除を再帰的に行います.

Besides functions for opening, reading and writing files, the File module has many functions that work on the file system. Those functions are named after their UNIX equivalents. For example, File.rm/1 can be used to remove files, File.mkdir/1 to create directories, File.mkdir_p/1 creates directories guaranteeing their parents exists and there is even File.cp_r/2 and File.rm_rf/2 which copy and remove files and directories recursively.

Fileモジュールの関数には2種類の慣習があることに気づくでしょう,1つは!(バンと読みます)つきのもので,もう1つはつかないものです.例えば"hello"というファイルを読むとき,上の例で!がつかない方を既に使いましたね.新しい例をいくつか試してみましょう:

You will also notice that functions in the File module have two variants, one with ! (bang) in its name and others without. For example, when we read the "hello" file above, we have used the one without !. Let's try some new examples:

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

ファイルが無い場合,!がついたバージョンではエラーが発生していますね.つまり!のつかないバージョンはパターンマッチングを利用して異なる形式の結果を取り扱おうとするときに選びます.しかし,もしファイルがそこにあること期待しているなら,バンつきの方が例外のエラーメッセージがわかりやすいので便利です.つまり絶対にこう書かないでください:

Notice that when the file does not exist, the version with ! raises an error. That said, the version without ! is preferred when you want to handle different outcomes with pattern matching. However, if you expect the file to be there, the bang variation is more useful as it raises a meaningful error message. That said, never write:

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

そのかわりにこう書いてください:

Instead write:

case File.read(file) do
  {:ok, body} -> # handle ok
  {:error, r} -> # handle error
end

あるいはこうです

or

File.read!(file)

12.3 パスモジュール - The Path module

ファイルモジュールの関数の多くは引数としてパスを期待しています.たいていの場合,これらのパスはバイナリでありPathモジュールで操作できます:

The majority of the functions in the File module expects paths as arguments. Most commonly, those paths will be binaries and they can be manipulated with the Path module:

iex> Path.join("foo", "bar")
"foo/bar"
iex> Path.expand("~/hello")
"/Users/jose/hello"

ここまでで,ファイルシステムを使った入出力の主なモジュールについてやりました.次に,なじみの少ない,進んだ入出力の話題について話していきましょう.このセクションはElixirのコードを書く必要がありません,ですから気軽に飛ばしてしまってもよいです,ただ,VMで入出力システムがどのように実装されているかの概要や,その他VMのおもしろい点についてお伝えします.

With this we have covered the main modules for doing IO and interacting with the file system. Next we will discuss some curiosities and advanced topics regarding IO. Those sections are not necessary to write Elixir code, so feel free to skip them, but they do provide an overview of how the IO system is implemented in the VM and other curiosities.

12.4 プロセスとグループリーダー - Processes and group leaders

File.open/2がPIDを含んだタプルを返すことを覚えているかもしれません:

You may have noticed that File.open/2 returned a tuple containing a PID:

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

これは入出力モジュールがプロセスの中で実際に動いているためです.IO.write(pid, binary)と書くと,入出力モジュールはメッセージをやりたい操作と共にプロセスへ送ります.自身のプロセスを使ってみたときに何が起こるかみてみましょう:

That's because the IO module actually works with processes. When you say IO.write(pid, binary), the IO module will send a message to the process with the desired operation. Let's see what happens if we use our own process:

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>, #PID<0.57.0>, {:put_chars, :unicode, "hello"}}
** (ErlangError) erlang error: :terminated

IO.write/2のあと,入出力モジュールへ表示したい内容が送られているのがわかります.次に,入出力モジュールが何か結果を期待した時点で失敗しています.なぜなら(入出力デバイスを)供給していないためです.

After IO.write/2, we can see the request sent by the IO module printed, which then fails since the IO module expected some kind of result that we did not supply.

StringIOモジュールは文字列で入出力デバイスのメッセージを実装したものを提供します:

The StringIO module provides an implementation of the IO device messages on top of a string:

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

入出力デバイスとプロセスをモデル化することで,Erlang VMは同じネットワークの異なるノード間でファイルプロセスを交換し,どちらのノードであってもファイルを読み書きできます.全ての入出力デバイスで,どのプロセスにも1つだけグループリーダーと呼ばれる特別なものがあります.

By modelling IO devices with processes, the Erlang VM allows different nodes in the same network to exchange file processes to read/write files in between nodes. Of all IO devices, there is one that is special to each process, called group leader.

:stdioと書いた場合,実際にはメッセージをグループリーダーへ送っています.グループリーダーはSTDIOファイルディスクリプタへと書きこみます:

When you write to :stdio, you are actually sending a message to the group leader, which writes to STDIO file descriptor:

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

グループリーダーはプロセス毎に設定でき,異なる状況で利用されます.例えばリモート端末でコードを実行する際,リモートノードで表示されたメッセージはリダイレクトされ,リクエストするきっかけになった端末で表示されることを保証します.

The group leader can be configured per process and is used in different situations. For example, when executing code in a remote terminal, it guarantees messages in a remote node are redirected and printed in the terminal that triggered the request.

12.5 iodatachardata - iodata and chardata

上でみた全ての例ではファイルへの書き込みにバイナリ/文字列を利用していました.この章のタイトルは"Binaries, strings and char lists"です,以前に,文字列が単なるバイトである一方文字リストはコードポイントのリストであると言いました.

In all examples above, we have used binaries/strings when writing to files. In the chapter "Binaries, strings and char lists", we mentioned how strings are simply bytes while char lists are lists with code points.

IOFileの関数は引数としてリストも取れます.それだけではなく,数値とバイナリが混在したリストのリストも取れます:

The functions in IO and File also allow lists to be given as arguments. Not only that, they also allow a mixed list of lists, integers and binaries to be given:

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

しかし,これには少し気をつけないといけません.リストは,IOデバイスのエンコーディングの依存を利用して,バイトのかたまり,あるいは文字のかたまりで表現されます.もしファイルがエンコーディングなしで開かれたら,ファイルはrawモードを期待しており,IOモジュールの関数は必ずbin*ではじまるものが使われます.それらの関数は引数としてiodataを期待しています,つまりバイト及びバイナリからなるリストが渡されることを期待しているということです.

However, this requires some attention. A list may represent either a bunch of bytes or a bunch of characters and which one to use depends on the encoding of the IO device. If the file is opened without encoding, the file is expected to be in raw mode, and the functions in the IO module starting with bin* must be used. Those functions expect an iodata as argument, i.e. it expects a list of integers representing bytes and binaries to be given.

一方で:stdio:utf8のエンコーディングを指定して開いたファイルはIOモジュールの残り(訳注bin*ではじまらない?)関数で動き,引数としてchar_dataを期待します,つまり文字と文字列からなるリストが渡されることを期待しているということです.

On the other hand, :stdio and files opened with :utf8 encoding work with the remaining functions in the IO module and those expect a char_data as argument, i.e. they expect a list of characters or strings to be given.

とはいえこれはちょっとした違いにすぎず.リストをそれらの関数へ渡そうと意図している場合にだけ詳しく検討すればよいです.バイナリは既にバイト表現されているので,常にrawで扱うことができます.

Although this is a subtle difference, you only need to worry about those details if you intend to pass lists to those functions. Binaries are already represented by the underlying bytes and as such their representation is always raw.

これで入出力デバイスと入出力に関連した機能をざっと見終わりました.IOFilePathStringIOというElixirのモジュールについてや,どのようにVMが入出力の機構をうまく使うか,どのように入出力の操作に(charとioの)データを利用するかについて学びました.

This finishes our tour of IO devices and IO related functionality. We have learned about four Elixir modules, IO, File, Path and StringIO, as well as how the VM uses processes for the underlying IO mechanisms and how to use (char and io) data for IO operations.