URL
試した環境
- Ubuntu Server 14.04 LTS
- Erlang/OTP 18
- Elixir 1.0.4
IO and the file system
この章は、IO
, File
, Path
のようなモジュールに関連するinput/outputのメカニズムのとファイルシステムに関する処理の導入。
The IO module
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
The FIle module
FileモジュールはIOデバイスとしてファイルを開く関数を含んでいる。
デフォルトでは、ファイルはIOモジュールの IO.binread/2
と IO.binwrite/2
を使うことで、開発者が必要とするバイナリモードで開かれる。
iex> {:ok, file} = File.open "hello", [:write]
{:ok, #PID<0.63.0>}
iex> IO.binwrite file, "world"
:ok
iex> File.close file
:ok
iex> File.read "hello"
上の例では、helloファイルが生成され、"world"がそのファイルに書き込まれる。
ファイルは、utf8エンコーディングバイトとして、ファイルからバイトを解釈するFileモジュールに指定することで、utf8エンコーディングで開くことができる。
さらに、ファイルを開く、読み込む、書き込むための関数として、File
モジュールはファイルシステムと連動して動作する多くの関数を持っている。これらの関数は、UNIXにちなんで名前が付けられている。例えば、File.rm/1
はファイルを削除するために使われ、File.mkdir/1
はディレクトリを作成するときに使われ、File.mkdir_p/1
はディレクトリと連続する親ディレクトリも作成する。再帰的にディレクトリをコピーと削除をするFile.cp_r/2
と File.rm_rf/2
さえもある。
Fileモジュールの関数は、2つの変形があることに気づくだろう。1つは標準形、1つは標準形版と同じ名前の!
を持つもう1つの変形。例えば、"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
(elixir) lib/file.ex:245: File.read!/1
ファイルが存在しない場合、!
版はエラーが起こることがわかる。!
なし版は、パターンマッチを使って異なる結果を扱いたい場合におすすめ。
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
を使うと良い。(例えば、エラーをバブルアップさせたい場合)
The Path module
Fileモジュールの関数の多くは引数としてパスを期待する。たいてい、これらのパスは通常のバイナリ。Path
モジュールは次のようなパスで動作するように便宜をはかってくれる
iex(6)> Path.join("foo", "bar")
"foo/bar"
iex(7)> Path.expand("~/hello")
"/home/storkibis/hello"
バイナリを単に操作することに対して、Path
モジュールの関数を使うことは、オペレーティングシステムの差異を埋めてくれるため、おすすめ。
例えば、Path.join/2
はUNIXライクなシステムでは/
で結合し、Windowsシステムでは¥
で結合する。
これで、IOとファイルシステムとの対話を扱うのに、Elixirが提供する主要なモジュールをカバーした。次のセクションでは、IOに関してより進んだトピックを議論する。これらのセクションはElixirコードを書くためには必要ではない、そう感じればスキップしても良い。ただし、VMにIOシステムがどのように実装されているかの概要や興味深い話をする。
Processes and group leaders
File.open/2
は{:ok, pid}
のようなタプルを返却することに気づいただろう。
iex> {:ok, file} = File.open "hello", [:write]
{:ok, #PID<0.75.0>}
これはIOモジュールは実際にはプロセスで動作しているため。IO.write(pid, binary)
を書いた時、IOモジュールはpid
によって識別されるプロセスにやりたい操作のメッセージを送信している。もし自分自身のプロセスを使った場合どうなるかみる。
iex> pid = spawn fn ->
...> receive do: (msg -> IO.inspect msg)
...> end
#PID<0.79.0>
iex> IO.write(pid, "hello")
{:io_request, #PID<0.59.0>, #Reference<0.0.1.155>,
{:put_chars, :unicode, "hello"}}
** (ErlangError) erlang error: :terminated
(stdlib) :io.put_chars(#PID<0.79.0>, :unicode, "hello")
IO.write/2
の後、IOモジュールによって、出力されたリクエストを送っている(4つの要素のタプル)。その後すぐに、IOモジュールは期待したある種類の結果が供給されず失敗する。
StringIO
モジュールは文字列の上に、IOデバイスメッセージの実装を提供する。
iex> {:ok, pid} = StringIO.open("hello")
{:ok, #PID<0.61.0>}
iex> IO.read(pid, 2)
"he"
プロセスでのIOデバイスのモデリングによって、Erlang VMで、同じネットワーク上の異なったノードにノード間でのファイルのread/writeするためfileプロセスのやり取りをできるようにする。全てのIOデバイスの、各プロセスで、group leaderという特別なプロセスが1つある。
:stdio
と書いた時、実際には標準出力のファイルディスクリプタへ書き込むgroup leaderへメッセージを送信している。
iex> IO.puts :stdio, "hello"
hello
:ok
iex> IO.puts Process.group_leader, "hello"
hello
:ok
group leaderはプロセス毎の設定をができ、異なる状況で使われる。
例えば、リモートターミナルでコードを実行するとき、group lodaderはリモートノードでメッセージをリクエストをトリガーしたターミナルにリダイレクトして、画面への出力をすることを保証する。
iodata and chardata
上記のすべての例で、ファイルを記載するときにバイナリを使った。Binaries, strings and char listsの章では、文字列は単なるバイトで、一方で文字のリストコードポイントであることに言及した。
IOとFileモジュールの関数は引数としてリストを与えることを許している。それだけではなく、リスト、数値とバイナリが混ざったものリストを与えることも許している。
iex> IO.puts 'hellow world'
hellow world
:ok
iex> IO.puts ['hello', ?\s, "world"]
hello world
:ok
しかし、これはある注意が必要。リストは、IOデバイスのエンコーディングの依存を利用して、バイトの束、または文字の束で表現される。
もし、ファイルがエンコーディング無しで開かれた場合、ファイルはrawモードであることが期待され、bin*
で開始されるIOモジュールの関数が使われる。
これらの関数は、iodata
を引数として期待する。例えば、バイトやバイナリで与えられる数値表現のリストを期待する。
他方、:stdio
や:utf8
エンコーディングで開いたファイルは、IOモジュールの残りの関数で動作する。
これらの関数は引数としてchar_data
を期待する。これは文字のリストや文字列のことである。
これは微妙な違いではあるが、もしこれかの関数にリストを渡すのであれば、これらの詳細について心配をする必要がある。バイナリは常に基本となるバイトで表現される、それらのような表現は常にrawである。