本稿はElixir公式サイトの許諾を得て「IO and the file system」の解説にもとづき、加筆補正を加えて、Elixirにおける入出力の仕方やファイルシステムに関わる操作、IO
、File
、Path
などの関連モジュールを簡単に紹介します。
IOモジュール
IO
モジュールは、入出力のためのおもな機能を提供します。読み書き先は、標準入出力(:stdio
)や標準エラー(:stderr
)、ファイル、その他の入出力デバイスなどです。
IO.puts/2
は、引数の項目を出力します。IO.gets/2
は入出力デバイスからの読み込みです。ともに第1引数のデバイスは、標準入出力(:stdio
)がデフォルトになっています。
iex> IO.puts "hello world"
hello world
:ok
iex> IO.gets "yes or no? "
yes or no? hello #<- helloとタイプして[enter]
"hello\n"
第1引数に:stderr
を与えると、標準エラーが入出力先になります。
iex> IO.puts :stderr, "hello world"
hello world
:ok
Fileモジュール
File
モジュールには、入出力デバイスとしてファイルを扱うための関数が備わっています。つぎのコードは、ファイルを開け閉じして、文字列を読み書きする例です。ファイルはデフォルトではバイナリのモード(:binary
)で開かれます。データの読み書きには用いるのは、IO
モジュールの関数です。
iex> {:ok, file} = File.open("hello", [:write])
{:ok, #PID<0.90.0>}
iex> IO.binwrite(file, "world")
:ok
iex> File.close(file)
:ok
iex> File.read("hello")
{:ok, "world"}
iex> {:ok, file} = File.open("hello", [:read])
{:ok, #PID<0.95.0>}
iex> IO.binread(file, :line)
"world"
iex> File.close(file)
:ok
-
File.open/2
: 第1引数のパスのファイルを開きます。第2引数はモードのリストで、:write
は書き込み、:read
は読み込みです。 -
File.close/1
: 引数のファイルを閉じます。 -
File.read/1
:{:ok, binary}
のタプルを返します。binary
は引数のパスのバイナリデータです。読み込めなかったときは{:error, reason}
が返ります。 -
IO.binwrite/2
: 第1引数がデバイスで、デフォルト値は標準入出力の:stdio
です。第2引数の項目をバイナリとして書き込みます。 -
IO.binread/2
: 第1引数がデバイスで、デフォルト値は標準入出力の:stdio
です。第2引数で読み込むのが1行(:line
)かすべて(:all
)かを定めます。
ファイルを開くモードには: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
モジュールの関数には、同じ名前で最後に!
記号のつくものとつかないものがあることに気づくでしょう。たとえば、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:310: File.read!/1
タプルを返す関数は、戻り値にパターンマッチングが使えます。
defmodule Example do
def read_file(file) do
case File.read(file) do
{:ok, body} -> IO.puts(body)
{:error, reason} -> IO.puts("error: #{reason}")
end
end
end
iex> Example.read_file("hello")
world
:ok
iex> Example.read_file("unknown")
error: enoent
:ok
ファイルが存在する前提であれば、File.read!/1
の方が端的です。万が一ファイルがなかったとき、パターンマッチングではマッチしないというエラーになります。File.read!/1
であれば、エラーメッセージにファイルの存在しないことが示されるからです。
iex> {:ok, body} = File.read("unknown")
** (MatchError) no match of right hand side value: {:error, :enoent}
Pathモジュール
File
モジュールの多くの関数は引数にパスが含まれます。パスは通常バイナリです。Path
モジュールには、パスとしてバイナリを扱うための関数が備わっています。
Path.join/2
は、ふたつの引数をパスとしてつなぎます。また、Path.expand/1
はパスを展開して絶対パスにします。
iex> Path.join("elixir", "test")
"elixir/test"
iex> Path.expand("/elixir/test/../config")
"/elixir/config"
パスの扱いは、文字列を直に操作するのでなく、Path
モジュールの関数を使う方がよいでしょう。オペレーティングシステムの違いが吸収されるからです。ファイルを操作するとき、パスの中のスラッシュ/
は、Windowsでは自動的にバックスラッシュ\
に変換されます。
入出力とファイルシステムの操作に関わるおもなモジュールのご説明は以上です。このあとは、入出力についての少し進んだ話題を扱います。Elixirのコードを書くための解説ではないので、飛ばしても構いません。VMの中でIOシステムがどのように実装されているかを解説します。
プロセスとグループリーダー
File.open/2
はタプルを返します。これはIO
モジュールが、内部的にプロセスに働きかけるからです。
iex> {:ok, file} = File.open "hello", [:write]
{:ok, #PID<0.47.0>}
たとえば、IO.write/2
を使うと、 IO
モジュールがPIDで参照されるプロセスにメッセージを送り、操作が行われます。出力される4要素のタプルがそのメッセージです。そのあと、IO
モジュールの求める結果が与えられなかったために失敗しています。
iex> pid = spawn fn ->
...> receive do: (msg -> IO.inspect msg)
...> end
#PID<0.89.0>
iex> IO.write(pid, "hello")
{:io_request, #PID<0.84.0>, #Reference<0.3003912951.510132227.213667>,
{:put_chars, :unicode, "hello"}}
** (ErlangError) Erlang error: :terminated
(stdlib) :io.put_chars(#PID<0.89.0>, :unicode, "hello")
StringIO
モジュールは、文字列に入出力デバイスの操作を実装します。StringIO
は入出力デバイスになり、IO
モジュールの関数が使えるのです。StringIO.open/2
は入出力デバイスをつくり、IO.read/2
はその参照から第2引数の文字数を読み込みます。
iex> {:ok, pid} = StringIO.open("hello")
{:ok, #PID<0.96.0>}
iex> IO.read(pid, 4)
"hell"
Erlang VMは入出力デバイスをプロセスでモデル化することにより、同じネットワークの異なるノードがファイルの操作をやり取りして、ノード間でファイルを読み書きできるようにしています。
すべてのIOデバイスは、プロセスにひとつ特別なグループリーダーをもちます。:stdio
に書き込みをしたとき、メッセージはグループリーダーに送られるのです。グループリーダーは標準出力ファイル記述子に書き込みます。Process.group_leader/0
は、呼び出しもとプロセスのグループリーダーのPIDを返します。
iex> IO.puts :stdio, "hello"
hello
:ok
iex> IO.puts Process.group_leader, "hello"
hello
:ok
グループリーダーはプロセスごとにつくられ、さまざまな状況で使われます。たとえば、リモート端末でコードを実行したとき、リモートノード内のメッセージは、リクエストを発した端末にリダイレクトされて出力されることが保証されます。
入出力データと文字データ
Elixirにおける文字列はバイトの集まりで、文字リストはUnicodeのコードポイントを納めたリストです(「Elixir入門 06: バイナリと文字列および文字リスト」参照)。
IO
とFile
モジュールの関数には、引数にリストも与えられます。それだけでなく、リストに整数や入れ子リスト、さらにバイナリを混在させても構いません。
iex> IO.puts '拝啓' ++ [25964, 20855]
拝啓敬具
:ok
iex> IO.puts [104, 'ello', ?\s, "world"]
hello world
:ok
ただし、入出力の操作にリストを使うときは、注意しなければなりません。リストはバイトの集まりも、文字列も示せるからです。どちらを用いるかは、入出力デバイスのエンコーディングによります。
エンコーディングを定めずに開かれたファイルは、rawモードとみなされます。IO
モジュールから使う関数は、bin
で始まるものでなければなりません。これらの関数は、引数に入出力データを受け取ります。つまり、バイトとバイナリを表す整数のリストとして扱うのです。
:stdio
と:utf8
のモードにより、UTF-8エンコーディングで開いたファイルは、bin
のつかないIO
モジュールの関数も使えるようになります。これらの関数は引数に文字データを受け取ります。つまり、文字または文字列のリストとして扱うのです。
細かな違いとはいえ、入出力の関数にリストを渡すときには注意してください。バイナリはすでにバイトにもとづいて表されているので、つねにrawモードで扱われます。
Elixir入門もくじ
- Elixir入門 01: コードを書いて試してみる
- Elixir入門 02: 型の基本
- Elixir入門 03: 演算子の基本
- Elixir入門 04: パターンマッチング
- Elixir入門 05: 条件 - case/cond/if
- Elixir入門 06: バイナリと文字列および文字リスト
- Elixir入門 07: キーワードリストとマップ
- Elixir入門 08: モジュールと関数
- Elixir入門 09: 再帰
- Elixir入門 10: EnumとStream
- Elixir入門 11: プロセス
- Elixir入門 12: 入出力とファイルシステム
- Elixir入門 13: aliasとrequireおよびimport
- Elixir入門 14: モジュールの属性
- Elixir入門 15: 構造体
- Elixir入門 16: プロトコル
- Elixir入門 17: 内包表記
- Elixir入門 18: シギル
- Elixir入門 19: tryとcatchおよびrescue
- Elixir入門 20: 型の仕様とビヘイビア
- Elixir入門 21: デバッグ
- Elixir入門 22: Erlangライブラリ
- Elixir入門 23: つぎのステップ
Top comments (3)
Could you provide another URL for your post so as to support Google translate to English?
Is the following url what you would like? Unfortunately, Google translate did not work for dev.to site.
translate.weblio.jp/web/english?lp...
Thanks, the translation is nice.
Maybe you can post it to other site and try Google translate.
Who are your readers expected? I think most here cannot read Japanese, although I wanna learn Japanese because I'm a fan of Matz.