DEV Community

gumi TECH for gumi TECH Blog

Posted on • Updated on

Elixir入門 12: 入出力とファイルシステム

本稿はElixir公式サイトの許諾を得て「IO and the file system」の解説にもとづき、加筆補正を加えて、Elixirにおける入出力の仕方やファイルシステムに関わる操作、IOFilePathなどの関連モジュールを簡単に紹介します。

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"
Enter fullscreen mode Exit fullscreen mode

第1引数に:stderrを与えると、標準エラーが入出力先になります。

iex> IO.puts :stderr, "hello world"
hello world
:ok
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
  • 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/1File.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
Enter fullscreen mode Exit fullscreen mode

タプルを返す関数は、戻り値にパターンマッチングが使えます。

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
Enter fullscreen mode Exit fullscreen mode
iex> Example.read_file("hello")
world
:ok
iex> Example.read_file("unknown")
error: enoent
:ok
Enter fullscreen mode Exit fullscreen mode

ファイルが存在する前提であれば、File.read!/1の方が端的です。万が一ファイルがなかったとき、パターンマッチングではマッチしないというエラーになります。File.read!/1であれば、エラーメッセージにファイルの存在しないことが示されるからです。

iex> {:ok, body} = File.read("unknown")
** (MatchError) no match of right hand side value: {:error, :enoent}
Enter fullscreen mode Exit fullscreen mode

Pathモジュール

Fileモジュールの多くの関数は引数にパスが含まれます。パスは通常バイナリです。Pathモジュールには、パスとしてバイナリを扱うための関数が備わっています。

Path.join/2は、ふたつの引数をパスとしてつなぎます。また、Path.expand/1はパスを展開して絶対パスにします。

iex> Path.join("elixir", "test")
"elixir/test"
iex> Path.expand("/elixir/test/../config")
"/elixir/config"
Enter fullscreen mode Exit fullscreen mode

パスの扱いは、文字列を直に操作するのでなく、Pathモジュールの関数を使う方がよいでしょう。オペレーティングシステムの違いが吸収されるからです。ファイルを操作するとき、パスの中のスラッシュ/は、Windowsでは自動的にバックスラッシュ\に変換されます。

入出力とファイルシステムの操作に関わるおもなモジュールのご説明は以上です。このあとは、入出力についての少し進んだ話題を扱います。Elixirのコードを書くための解説ではないので、飛ばしても構いません。VMの中でIOシステムがどのように実装されているかを解説します。

プロセスとグループリーダー

File.open/2はタプルを返します。これはIOモジュールが、内部的にプロセスに働きかけるからです。

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

たとえば、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")
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

グループリーダーはプロセスごとにつくられ、さまざまな状況で使われます。たとえば、リモート端末でコードを実行したとき、リモートノード内のメッセージは、リクエストを発した端末にリダイレクトされて出力されることが保証されます。

入出力データと文字データ

Elixirにおける文字列はバイトの集まりで、文字リストはUnicodeのコードポイントを納めたリストです(「Elixir入門 06: バイナリと文字列および文字リスト」参照)。

IOFileモジュールの関数には、引数にリストも与えられます。それだけでなく、リストに整数や入れ子リスト、さらにバイナリを混在させても構いません。

iex> IO.puts '拝啓' ++ [25964, 20855]
拝啓敬具
:ok
iex> IO.puts [104, 'ello', ?\s, "world"]
hello world
:ok
Enter fullscreen mode Exit fullscreen mode

ただし、入出力の操作にリストを使うときは、注意しなければなりません。リストはバイトの集まりも、文字列も示せるからです。どちらを用いるかは、入出力デバイスのエンコーディングによります。

エンコーディングを定めずに開かれたファイルは、rawモードとみなされます。IOモジュールから使う関数は、binで始まるものでなければなりません。これらの関数は、引数に入出力データを受け取ります。つまり、バイトとバイナリを表す整数のリストとして扱うのです。

:stdio:utf8のモードにより、UTF-8エンコーディングで開いたファイルは、binのつかないIOモジュールの関数も使えるようになります。これらの関数は引数に文字データを受け取ります。つまり、文字または文字列のリストとして扱うのです。

細かな違いとはいえ、入出力の関数にリストを渡すときには注意してください。バイナリはすでにバイトにもとづいて表されているので、つねにrawモードで扱われます。

Elixir入門もくじ

番外

Discussion (3)

Collapse
chenge profile image
chenge

Could you provide another URL for your post so as to support Google translate to English?

Collapse
fumiononaka profile image
Fumio Nonaka

Is the following url what you would like? Unfortunately, Google translate did not work for dev.to site.
translate.weblio.jp/web/english?lp...

Collapse
chenge profile image
chenge

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.