データ構造の使い分け - Elixir チートシート - [翻訳記事]

nabbisen - Sep 22 '18 - - Dev Community

本記事は、以下の記事の翻訳です:
Which Data Structure Should I Use? An Elixir Cheat Sheet by Tracy Lum
* 執筆者に許諾を頂いた上で掲載しています。


Elixir の初心者として考えていることがあります。
Elixir を始めるにあたって、最も苦労することの一つは、どのデータ構造を使うべきかを理解することだ、ということです。
私の所属するチームでは最近 Elixir ですべてのことを行おうと取り組み始めています。
そのようなわけで、私は現在、本格的に勉強をやり直しています。
しかしながら、チームのコードを読み込んでいる中でしばしば直面することなのですが、自分がいま目にしているものを理解することさえ難しいことがあります。
Elixir の文法は Ruby に非常によく似ています。
(私にとって、とても良く知っている言語です。)
しかしそのパターンと、規約と、データ構造が、 少しばかり 違うのです。
私は頭ではそのことを理解しています。
Elixir は、オブジェクト志向では無く、関数型の言語だからです:
あなたは、Ruby ではオブジェクトを使う場面において、Elixir では代わりにプロセスの生成を使うことになるでしょう。

それはともかくとして、私はいま Elixir を勉強している最中です。
その中で、次のようなものがあれば役に立つだろうと考えました。
すなわち、チートシートと、Ruby 使いとして Elixir について調べる過程で気付いた、データ構造に関する相違点をまとめたもの、です。

データ型

もしも Ruby(あるいは他の多くのプログラム言語)を学んだことがあるのであれば、整数型 / 浮動小数点数型 / 範囲(レンジ) / 正規表現は、きっといずれも馴染みのあるものでしょう。
幸いなことに、これらはすべて Elixir にも存在します。
わずかな違いがあるにはありますが、それらにぶつかったことは、私の経験の中ではまだあまりありません。

アトムは Ruby のシンボルのようなものです。
コロンで始まり、その名前がアトムの値になります。
例えば :hello は Elixir におけるまっとうなアトムです。
タグの値として使用されたりします。

Elixir にも文字列はあります。
文字列は必ず二重引用符で囲われます。
一方で、文字のリストは一重引用符で囲われます。
文字列はバイナリデータですが、文字のリストは実際にはただのコードポイントのリストです。
私はこれまでに文字のリストを使用したことはほとんどありません。

ここで、以上の型の表現について、さっと見てみましょう。

iex> 2         # integer            : 整数型
iex> 2.0       # floating point     : 浮動小数点数型
iex> false     # boolean            : 真偽値
iex> 1..4      # range              : 範囲
iex> ~r/hello/ # regular expression : 正規表現
iex> :hello    # atom               : アトム
iex> "world"   # string             : 文字列
iex> 'world'   # charlist           : 文字のリスト
Enter fullscreen mode Exit fullscreen mode

Elixir にはこれら以外にもデータ型が存在します。
ポート(Port)と PID です。
プロセスのやり取りで使用されます。
Erlang VM 上で利用可能なエンティティです。

ポート

ポート(Port)が使われるのは、アプリケーション外部のリソースとやり取り(読み取り / 書き込み)を行う時です。
オペレーティング・システムのプロセスを起動してそれらとやり取りをすることが多いです。
例えばポートを開いて OS の echo のようなコマンドを実行する場合が該当します。

ポートを開いて相手にメッセージを送るには、次のようにします:

iex> port = Port.open({:spawn, "echo sup"}, [:binary])
#Port<0.1305>
Enter fullscreen mode Exit fullscreen mode

その後に IEx.Helpers モジュールの flush() を実行すれば、ポートから受信したメッセージを出力することができます。

iex> port = Port.open({:spawn, "echo sup"}, [:binary])
#Port<0.1305>
iex> flush()
iex> {#Port<0.1305>, {:data, "sup\n"}}
iex> :ok
Enter fullscreen mode Exit fullscreen mode

ポートに対しては、任意の表現をバイナリデータで送信して実行することができます。
例えば、私の jekyll ブログのディレクトリから、iex セッションを立ち上げて、ポートを開いて、そこから bundel install コマンドを送信してみましょう。
Ruby の gem における依存関係がすべてインストールされました。
こちらが出力内容の一部です。

iex> port = Port.open({:spawn, "bundle install"}, [:binary])
#Port<0.1306>
iex> flush()
{#Port<0.1306>, {:data, "Using concurrent-ruby 1.0.5\n"}}
{#Port<0.1306>, {:data, "Using i18n 0.9.5\n"}}
{#Port<0.1306>, {:data, "Using minitest 5.11.3\n"}}
{#Port<0.1306>, {:data, "Using thread_safe 0.3.6\n"}}
{#Port<0.1306>, {:data, "Using tzinfo 1.2.5\n"}}
{#Port<0.1306>, {:data, "Using activesupport 4.2.10\n"}}
{#Port<0.1306>, {:data, "Using public_suffix 2.0.5\n"}}
{#Port<0.1306>, {:data, "Using addressable 2.5.2\n"}}
{#Port<0.1306>, {:data, "Using bundler 1.16.2\n"}}
{#Port<0.1306>, {:data, "Using coffee-script-source 1.11.1\n"}}
{#Port<0.1306>, {:data, "Using execjs 2.7.0\n"}}
{#Port<0.1306>,
 {:data,
  "Bundle complete! 4 Gemfile dependencies, 85 gems now installed.\nUse `bundle info [gemname]` to see where a bundled gem is installeそれから d.\n"}}
:ok
Enter fullscreen mode Exit fullscreen mode

PID

PID はプロセスへの参照です。
新しいプロセスを生成した時には、常に新しい PID が返されて来ます。
PID については、お話したいことがたくさんあります。
他のプロセスへメッセージを送れるようになるためには、PID について理解するのに時間を掛けて取り組まねばならないでしょう。

プロセスを生成した結果として返されて来た PID を受け取るサンプルを、以下に示します。

iex> pid = spawn fn -> IO.puts("hello world") end
iex> hello world
iex> #PID<0.123.0>
Enter fullscreen mode Exit fullscreen mode

プロセスは仕事を終えると死んで行きます。
PID と ポートは、外部から影響を受けずに動作することが保証されています。
しかし今のところは、それらが存在していることを意識するだけで十分だと思います。

それではこちらが、新たに出て来た型を追記した、基本となるチートシートです。

Elixir のデータ型のチートシート

iex> 2             # integer            : 整数型
iex> 2.0           # floating point     : 浮動小数点数型
iex> false         # boolean            : 真偽値
iex> 1..4          # range              : 範囲
iex> ~r/hello/     # regular expression : 正規表現
iex> :hello        # atom               : アトム
iex> "world"       # string             : 文字列
iex> 'world'       # charlist           : 文字のリスト
iex> #Port<0.1306> # port               : ポート
iex> #PID<0.123.0> # pid                : PID
Enter fullscreen mode Exit fullscreen mode

私の意見では、Elixir と本格的に取り組む上で必要なことは、これだけではありません。
これらの基本的なデータ型を構造体として使えるようにする方法を理解することが必要です。
そこで、様々なコレクション型と、それらを使うそれぞれの理由について、見て行きましょう。

コレクション型

あなたが目にすることになるであろうコレクション型は以下の通りです:

  • タプル
  • リスト
  • キーワードリスト
  • マップ
  • 構造体

これらの言葉は、きっと少なくとも一度は、過去にどこかで聞いたことがあるでしょう。
しかし、もしあなたが Ruby に慣れ親しんでいるならば、コレクション型を拡張したこれらのものがなぜすべて必要なのかということに、きっと疑問を持つはずです。
詳しく見て行きましょう。

タプル

タプルは順番を持つ値のコレクションです。
表現は以下の通りです:

iex> {:hello, "world"}
iex> {1, 2}
iex> {:ok, "this is amazing!", 2}

# タプルなのかどうかをチェックできます
iex> tuple = { "hello", "world"}
iex> is_tuple tuple
iex> true

# インデックスを指定することにより、タプルから要素を取り出すことができます
iex> elem(tuple, 1)
iex> "world"
Enter fullscreen mode Exit fullscreen mode

私の考えでは、タプルはいささか原始的です。
というのは、ハッシュとなるべきもののように見えるからです。
その一方で Ruby の配列のような振る舞いを見せることもあります。
それにも関わらず「タプル」(<数学> 組、順序組)と呼ばれています!
これらのことは慣れて来れば気にならなくなるだろうと、何度も混乱した末に自分に言い聞かせているところです。

タプルは Elixir のありとあらゆるところに顔を出します。
関数の戻り値がタプルになることもあります。
この場合はパターンマッチングを行うことができます。
そのようなことがあるので、タプルを通して Elixir の世界が見えて来るのだ、と理解しています。
タプルはたいてい 2 〜 4 つの要素を持ちます。
いまのところ、タプルは、私のお気に入りのデータ構造です。
4 つよりも多い要素を持つデータ構造を扱う場合は、きっと大体において、タプルでは無く、マップや構造体を使うのが良いはずです。

リスト

リストはリンクでつなげられたデータの構造体です。
表現は以下の通りです:

iex> [1, 2, 3, 4]
iex> ["hello", "world"]
Enter fullscreen mode Exit fullscreen mode

Ruby の配列だとあなたは思うかもしれません。
しかし Elixir においてはリストです!
リストはリンクでつなげられたデータ構造として実装されています。
そのため再帰処理に適しています。
その反面、ランダムに要素を取得したり、リスト長を計算したりするのには、不向きです。
なぜなら、リストのサイズを把握するためには、リスト全体を走査する必要があるからです。
私はいままでのところ、大抵の場面では、リストでは無くタプルを使って来ました。
この二者から一方を選ぶ必要がある場合、コレクションのサイズがどのくらいになるかを考慮した上で、どの種の操作が良いパフォーマンスを出すのかを検討しなければならないだろう、と思います。

キーワードリスト

さらに複雑な問題に対応するために、Elixir にはキーワードリストのようなものもあります。
これは本質的には、2 つの値を持つタプルから成る、リストです。

# キーワードリスト
iex> [ phrase: "oh hello", name: "tracy" ]

# 実際には 2 つの値を持つタプルたちです
iex> [ {:phrase, "oh hello"}, {:name, "tracy"} ]
Enter fullscreen mode Exit fullscreen mode

これが一般的なあり方なのだと意識はしていますが、このことにはいまも混乱させられています。
キーワードリストが優れているのは、同じキーの重複が許されるところです。

iex> keyword_list = [food: "peanut butter", food: "ice cream", flavor: "chocolate"] # まっとうなキーワードリストです
Enter fullscreen mode Exit fullscreen mode

キーワードリストは、コマンドラインにおける引数やオプションとしての利用に適しています。

マップ

次はマップの番です。
真のキーバリューストアを使いたいならば、キーとバリューから成るリストでは無く、マップが希望に叶うものです。
Ruby のハッシュに少し似た表現です:

iex> %{"greeting" => "hello", "noun" => "world"}
iex> %{:greeting => "hello", :noun => "world"}
iex> %{greeting: "hello", noun: "world"} # キーがアトムである場合、ハッシュロケット(=>)を省くことができます

iex> greeting = %{spanish: "hola", chinese: "ni hao", english: "hello"}
iex> greeting[:spanish]
iex> "hola"
iex> greeting.chinese
iex> "ni hao"
Enter fullscreen mode Exit fullscreen mode

マップは、連想データの送信をするのに適しています。
さらに、タプルで扱うのに適切なサイズを超えている、あらゆるものを扱うのに、非常に適しています。

構造体

構造体は拡張されたマップのようなものです。
キーの形式が厳密に定められており、アトムである必要があります。
構造体は、モジュールの中で、適切なデフォルト値とともに定義される必要があります。
ルールを持つマップと言えます。

iex> defmodule IceCream do
....   defstruct flavor: "", quantity: 0
.... end

iex> chocolate = %IceCream{flavor: "chocolate"}
iex> chocolate.flavor
iex> "chocolate"
Enter fullscreen mode Exit fullscreen mode

構造体は、マップと同じく、パーセント記号 % で定義されます。ただし構造体の場合は、記号の後ろにモジュールの名前が続きます。
この書式を見ると、構造体はルールの厳しくなったマップに過ぎないのだと、実感します。

Elixir の過去のバージョンでは HashDict というものが存在していたこともあります。
数百を超える値をマップで操作するために使われていました。
しかしこのモジュールは、古き良き Map があるために、非推奨となりました。

以上です。
Elixir で目にするであろう一般的なデータ型とコレクション型を見て来ました。
言語が変われば勝手の違うところはたくさん出て来ますが、共通するところもあります。学ばねばならないことは、もちろんたくさんあります。Elixir のこと、規約のこと、Elixir で実現できるすばらしいいくつものこと、についてです。
しかし(私の考えでは)以上のようなことから始めるのが、言語に馴れ親しむ上では、良いのでは無いでしょうか。
あなたはこれから Elixir に関して難解なことにぶつかるかもしれません。
この記事が、それらを解決するための良い道標になることがありましたら、幸いです!

Elixir のコレクション型のチートシート

iex> {:ok, "this is amazing!", 2}                                         # tuple        : タプル
iex> ["hello", "world"]                                                   # list         : リスト
iex> [ phrase: "oh hello", name: "tracy" ]                                # keyword list : キーワードリスト
iex> greeting = %{spanish: "hola", chinese: "ni hao", english: "hello"}   # map          : マップ
iex> chocolate = %IceCream{flavor: "chocolate"}                           # struct       : 構造体
Enter fullscreen mode Exit fullscreen mode

参考文献


お読み頂きどうもありがとうございました。

本記事は、以下の記事の翻訳です:
Which Data Structure Should I Use? An Elixir Cheat Sheet by Tracy Lum

To Tracy: Thank you so much for your kind permission for me to translate your post.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player