やっちのわいわい日記

電通大に編入した元高専生の日記。日記を書きつつ編入のこととか勉強のこととか書こうと思っています。編入とかで質問があるかたは@amhflcl0514にDMくださいお

Elixir勉強日記3 〜コレクション・Enum編〜

Elixir勉強日記第3回です

f:id:nishikino3:20190611145254j:plain

今回もElixir Schoolを引き続きやっていきます

elixirschool.com

今回は基本的なデータ構造をまとめてきいます

メソッドの説明について

初めにメソッドの説明する際の記法に関して説明します

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/1Erlangの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

ほかのメソッドたちは↓を見れば良さそう

hexdocs.pm

第3回まとめ

  • コレクション、リスト・キーワードリスト・タプル・マップについてまとめた
  • それぞれのコレクション長所や利点なんか生かして実装したい
  • Enumモジュールのメソッドを紹介した
  • いい感じに使えるようになりたい