Elixir勉強日記3 〜コレクション・Enum編〜
Elixir勉強日記第3回です
今回もElixir School
を引き続きやっていきます
今回は基本的なデータ構造をまとめてきいます
メソッドの説明について
初めにメソッドの説明する際の記法に関して説明します
Elixir(とその土台のErlang)において、関数や演算子の名前は2つの部分、与えられた名前とその アリティ から成ります。アリティはElixir(とErlang)のコードについて説明するときの中核となるものです。アリティは関数や演算子が取る引数の数です。名前とアリティはスラッシュで繋げられます。後ほどより詳しく扱いますが、この知識は今のところこの表記法を理解する助けになるでしょう。
要するに(メソッド名)/(数字)
という風に記述されていたらそのメソッド(メソッド名)
の引数の個数(数字)
ということになります
この(メソッド名)
には演算子
が入ることもあります(正確には演算子もメソッド)
Elixir
ではメソッドの引数の個数が異なると違うメソッドとして認識されるので気をつけましょう
リスト
リストは値の単純なコレクションで、複数の型を含むことができます。また、一意ではない値を含むことができます:
iex> [3.14, :pie, "Apple"] [3.14, :pie, "Apple"]
同一の型である必要がないのはruby
と類似してますね
Elixirはリストコレクションを連結リストとして実装しています。すなわちリストの長さを得るのは線形時間(O(n))の処理となります。このことから、リスト先頭への追加はほとんどの場合にリスト末尾への追加より高速です:
ほうほうつまりリスト => 線形リスト
なので末尾への処理より先頭の処理の方が早いということですね
iex> list = [3.14, :pie, "Apple"] [3.14, :pie, "Apple"] # リスト先頭への追加(高速) iex> ["π" | list] ["π", 3.14, :pie, "Apple"] # リスト末尾への追加(低速) iex> list ++ ["Cherry"] [3.14, :pie, "Apple", "Cherry"]
リストの連結
リストの連結には++/2演算子を用います
iex> [1, 2] ++ [3, 4, 1] [1, 2, 3, 4, 1]
文字列連結の演算子が<>
だったり、+
では無かったりするので使い分けの注意が必要ですね
リストの減算
減算に対応するために--/2演算子が用意されています。存在しない値を引いてしまっても安全です
iex> ["foo", :bar, 42] -- [42, "bar"] ["foo", :bar]
べんり
重複した値に注意してください。右辺の要素のそれぞれに対し、左辺の要素のうち初めて登場した同じ値が順次削除されます
iex> [1,2,2,3,2,3] -- [1,2,3,2] [2, 3]
はぇ
参考: リストの減算の値のマッチには strict comparison が使われています。
なるほど
頭部 / 尾部
iex> hd [3.14, :pie, "Apple"] 3.14 iex> tl [3.14, :pie, "Apple"] [:pie, "Apple"] # リストを頭部と尾部に分けるのにパターンマッチングやcons演算子(|)を使うこともできます iex> [head | tail] = [3.14, :pie, "Apple"] [3.14, :pie, "Apple"] iex> head 3.14 iex> tail [:pie, "Apple"]
タプル
タプルはリストに似ていますが、各要素はメモリ上に隣接して格納されます。このため、タプルの長さを得るのは高速ですが、修正を行うのは高コストとなります。というのも、新しいタプルは全ての要素がメモリにコピーされるからです。タプルは波括弧を用いて定義されます
タプルは関数から補助的な情報を返す仕組みとしてよく利用されます。この便利さは、パターンマッチングについて扱う時により明らかになるでしょう
iex> {3.14, :pie, "Apple"} {3.14, :pie, "Apple"} iex> File.read("path/to/existing/file") {:ok, "... contents ..."} iex> File.read("path/to/unknown/file") {:error, :enoent}
書き換えが遅いってことは定数とかに使う感じなのでしょうかね
キーワードリスト
Elixirでは、キーワードリストは最初の要素がアトムのタプルからなる特別なリストで、リストと同様の性能になります
iex> [foo: "bar", hello: "world"] [foo: "bar", hello: "world"] iex> [{:foo, "bar"}, {:hello, "world"}] [foo: "bar", hello: "world"]
キーワードリストの重要性は次の3つの特徴によって強調づけられています:
- キーはアトムです。
- キーは順序付けされています。
- キーの一意性は保証されません。
連想配列のkeyをタプルに限定してリストと同じように記述するとリストと同様な性能が得られるということなんでしょうかね
こうした理由から、キーワードリストは関数にオプションを渡すために非常に良く用いられます。
なるほど
マップ
Elixirではマップは”花形の”キーバリューストアで、キーワードリストとは違ってどんな型のキーも使え、順序付けされません。マップは%{}構文で定義することができます:
花形で草
iex> map = %{:foo => "bar", "hello" => :world} %{:foo => "bar", "hello" => :world} iex> map[:foo] "bar" iex> map["hello"] :world # 変数をマップのキーにすることができます iex> key = "hello" "hello" iex> %{key => "world"} %{"hello" => "world"} # アトムのキーだけを含んだマップには特別な構文があります iex> %{foo: "bar", hello: "world"} %{foo: "bar", hello: "world"} iex> %{foo: "bar", hello: "world"} == %{:foo => "bar", :hello => "world"} true # アトムのキーにアクセスするための特別な構文もあります iex> map = %{foo: "bar", hello: "world"} %{foo: "bar", hello: "world"} iex> map.hello "world" # マップのもう一つの興味深い特性は、マップの更新のための固有の構文があることです iex> map = %{foo: "bar", hello: "world"} %{foo: "bar", hello: "world"} iex> %{map | foo: "baz"} %{foo: "baz", hello: "world"}
マップの便利構文使いたかったらなるべくアトムをキーにしとけって気がした
%
忘れそう
小まとめ
リスト
- 単純なデータの羅列で複数の型に対応
- [1, 2, "hoge"]
タプル
- リストに類似するが、メモリ上に隣接して格納される.タプルを得るのは早いが修正を行うとすべてコピーしなおされため遅い
- {1, 2, "hoge"}
キーワードリスト
- アトムのタプルからなる特別なリストでリスト同様の性能
- [foo: "bar", hello: "world"]
- [{:foo, "bar"}, {:hello, "world"}]
マップ
- どんな型でもキーにできるキーバリューストア
- %{}
Enum
コレクションを列挙していくために用いる一連のアルゴリズム。
コレクションを制すぞい
Enumモジュールはおよそ70個以上の関数を含んでいます。タプルを除外した全てのコレクションは全て列挙可能です。Enumモジュールはおよそ70個以上の関数を含んでいます。前回のレッスンで学習した、タプルを除外した全てのコレクションは全て列挙可能です。
めんどうなので雑に列挙します
all?
all?を使うとき、そしてEnumの多くのケースで、コレクションの要素に適用する関数を渡します。all?の場合には、コレクション全体でこの関数はtrueと評価されなければならず、これを満たさない場合はfalseが返ります。
全部条件に合うえばtrue
any?
any?は少なくとも1つの要素がtrueと評価された場合にtrueを返します:
一つでも条件に合えばtrue
chunk_every
コレクションを小さなグループに分割する必要があるなら、恐らくchunk_every/2こそが探し求めている関数でしょう
&2で指定した長さのコレクションに分割
chunk_by
コレクションを要素数ではない何か他のものでグループにする必要がある場合には、chunk_by/2関数を使うことができます。この関数は列挙可能な値と関数を引数に取り、その関数の返り値が変わると新しいグループが始まります
iex> Enum.chunk_by(["one", "two", "three", "four", "five"], fn(x) -> String.length(x) end) [["one", "two"], ["three"], ["four", "five"]] iex> Enum.chunk_by(["one", "two", "three", "four", "five", "six"], fn(x) -> String.length(x) end) [["one", "two"], ["three"], ["four", "five"], ["six"]]
指定した方法でグルーピングできる
map_every
時にコレクションをグループに別けるだけでは充分ではない場合があります。
nth
毎のアイテムに対して何かの処理をしたい時にはmap_every/3が有用です。これは最初の要素にかならず触れます。
# 毎回3個を飛び越えながら関数を呼び出す iex> Enum.map_every([1, 2, 3, 4, 5, 6, 7, 8], 3, fn x -> x + 1000 end) [1001, 2, 3, 1004, 5, 6, 1007, 8]
一部の要素に対して処理したいときに使いそう
each
新しい値を生成することなく、コレクションを反復する必要があるかもしれません。こうした場合には
each/2
を使います
iex> Enum.each(["one", "two", "three"], fn(s) -> IO.puts(s) end) one two three :ok
map
関数を各要素に適用して新しいコレクションを生み出すには、map関数に目を向けましょう
iex> Enum.map([0, 1, 2, 3], fn(x) -> x - 1 end) [-1, 0, 1, 2]
人生で一番使いそう
min
コレクションの中で最小の(min/1)値を探します
min/2
も同様ですが、コレクションが空である場合、最小値を生成するための関数を渡します
max
minとおなじ
filter
filter/2
を使用するとコレクションで与えられた関数で評価して true になる要素のみを返すことができます。
絞り込むときに絶対につかう
reduce
reduce/3
を用いることで、コレクションをまとめ、そこから単一の値を抽出することができます。この処理を実行するにはオプションとしてアキュムレータ(積算器。この例では10)を関数に渡しますが、アキュムレータが与えられない場合にはコレクションの最初の値が用いられます:
iex> Enum.reduce([1, 2, 3], 10, fn(x, acc) -> x + acc end) 16 iex> Enum.reduce([1, 2, 3], fn(x, acc) -> x + acc end) 6 iex> Enum.reduce(["a","b","c"], "1", fn(x,acc)-> x <> acc end) "cba1"
コレクションを一つにまとめるのね完全に理解した
rubyだとinjectが近そう
sort
コレクションをソートするのは1つではなく、2つあるソート関数を使えば簡単です。
sort/1
はErlangのterm orderingを使ってソート順序を決めるというものです
これはシンプルに昇順になります
sort/2
には順序決めに使う関数を渡すことができます
# ソート関数あり iex> Enum.sort([%{:val => 4}, %{:val => 1}], fn(x, y) -> x[:val] > y[:val] end) [%{val: 4}, %{val: 1}] # なし iex> Enum.sort([%{:count => 4}, %{:count => 1}]) [%{count: 1}, %{count: 4}]
これは必要
uniq_by
uniq_by/2
を使ってコレクションから重複した要素を取り除くことができます
iex> Enum.uniq_by([1, 2, 3, 2, 1, 1, 1, 1, 1], fn x -> x end) [1, 2, 3]
uniq/1
もあったぷり
紹介されてる以外に使いそうなのはこんなところでしょうか
uniq/1
reverse/1
empty?/1
join/2
find/2
reject/2
with_index/1
ほかのメソッドたちは↓を見れば良さそう
第3回まとめ
- コレクション、リスト・キーワードリスト・タプル・マップについてまとめた
- それぞれのコレクション長所や利点なんか生かして実装したい
- Enumモジュールのメソッドを紹介した
- いい感じに使えるようになりたい