loading...
gumi TECH Blog

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

gumitech profile image gumi TECH Updated on ・4 min read

本稿は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"

第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/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

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

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: バイナリと文字列および文字リスト」参照)。

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

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入門もくじ

番外

Posted on by:

gumitech profile

gumi TECH

@gumitech

gumi TECH は、株式会社gumiのエンジニアによる技術記事公開やDrinkupイベントなどの技術者交流を行うアカウントです。 gumi TECH Blog: http://dev.to/gumi / gumi TECH Drinkup: http://gumitech.connpass.com

gumi TECH Blog

株式会社gumiのエンジニアによる技術記事を公開しています。

Discussion

pic
Editor guide
 

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.