やわらかテック

興味のあること。業務を通して得られた発見。個人的に試してみたことをアウトプットしています🍵

Elixirでqueue使って幅優先探索を実装する

Elixirにqueueがなかった件

しかし、Erlangには用意されていた
Erlang公式ドキュメントにばっちしqueueについて記述されている
Elixirで解決できない時はErlangのドキュメントを見るというのは本当ですね

Elixirからのqueueの使い方

新規のqueue作成
2つのリストを持つタプルの形式で値が返ってくる

que = :queue.new()

iex> que
{[], []}

queueに新たなデータを追加

new_que = :queue.in("okb", que)

iex> new_que
{["okb"], []}

先頭に待ち構えているデータの確認

iex> :queue.get(new_que)
"okb"

.getメゾットでは取得したデータが元のqueueに残っているままなので
取得したデータが削除されるようにするには以下のようにする

iex(12)> :queue.out(new_que)
{{:value, "okb"}, {[], []}}

取得に成功するとタプルの第1要素に:valueというタプルと共に取得した値が返ってくる
第2要素には取得後のqueueが返ってくる(今取得した値は含まれていない)

こいつにパターンマッチさせるにはこんな感じでいける

{{:value, value}, rest} = res
iex> value
"okb"

iex> rest
{[], []}

基本的に使うのはこのあたりなんじゃないかと勝手に思ってます
以上を踏まえてElixirで幅優先探索を実装します

ゆるく分かる幅優先探索について

すでにもっと分かりやすく解説してくださっている記事などたくさんあるのでざっくりと

僕の友人にプログラマーがいるか知りたいとします
この場合にどのように調べていくべきでしょうか

まずは連絡の取れる友人に一通り聞いて回り、プログラマーがいるかどうか確認します
もしこの時点でプログラマーを発見できれば探索は終了です(試行回数:1)

しかし、友人からプログラマーを発見できなかった場合は次の試行に移ります
次の手は自分の友人の友人にプログラマーがいるかどうかを確認します
友達の友達ってやつですね
この時点でプログラマーを発見できれば探索は終了です(試行回数:2)

見つからなかった場合は友達の友達の友達をあたります
この試行を発見できるまで、もしくは試行回数が上限に達するまで行います(試行回数:n)

このようにまずは近い(友人)ところを全て調べて
次に遠い(友人の友人)ところを順次、調べていくというのが
幅優先探索だと思っています

幅優先探索にはグラフが必要不可欠
友人関係というネットワークをグラフを使って表現するとこのような図になる
f:id:takamizawa46:20190502142907p:plain

またグラフをデータで表現するとこんな感じの構造になる

friends = %{
  okb: [:a, :b, :c, :d],
  a: [:e],
  b: [], #友人がいない(悲しいね)
  c: [:e, :f],
  d: [:f],
  e: [],
  f: [:g],
  g: []
}

Elixirで書いてみる

bfsというヘルパー関数を用いて中心のネットワーク位置を決定している
要は誰を中心として探索を行うかということを決めている

defmodule Algo do
  def bfs(graph, center, target) do
    #中心地点を決定し、queueを新規作成して追加
    que = :queue.in(graph[center], :queue.new())
    _bfs(graph, target, que)
  end
  defp _bfs(graph, target, que) do
    #queueから値を取り出して判定
    case :queue.out(que) do
      {{:value, val}, rest} ->
        if target in val do
          #対象のデータが見つかれば終了
          {:exist, target}
        else
          #取得したリストの要素を1つずつqueueに追加していく
          next_que = Enum.reduce(val, rest, fn key, que -> :queue.in(graph[key], que) end)
          _bfs(graph, target, next_que)
        end
      #queueが空になったら終了
      {:empty, _} -> :not_found
    end
  end
end

呼び出しはこんな感じになる

#グラフの用意(もっと面白いやつを作ればよかった)
graph = %{
    okb: [:a, :b, :c],
    b: [:d, :e],
    a: [:e],
    c: [:f, :g],
    d: [],
    e: [],
    f: [],
    g: []
}

res = Algo.bfs(graph, :okb,:f)
IO.inspect(res)

#result: {:exist, :f}

いつものように簡易テストを行なってみる

tests = [
  Algo.bfs(graph, :okb, :f) == {:exist, :f},
  Algo.bfs(graph, :okb, :h) == {:not_found},
  Algo.bfs(graph, :b, :f) == {:not_found},
  Algo.bfs(graph, :c, :f) == {:exist, :f}
]

IO.inspect(tests)
#result:
#[true, true, true, true]

いい感じですね
ただこのままだと無向グラフの場合に無限ループを引き起こす可能性があるので
一度queueに追加された値はqueueに追加しないように処理を追加します
既に追加された値はどうかはaccumを使って判定しています

defmodule Algo do
  def bfs(graph, center, target) do
    que = :queue.in(graph[center], :queue.new())
    _bfs(graph, target, que, [])
  end
  defp _bfs(graph, target, que, accum) do
    case :queue.out(que) do
      {{:value, val}, rest} ->
        if target in val do
          {:exist, target}
        else
          #accumに存在していないデータのみ抽出
          val = Enum.filter(val, fn x -> x not in accum end)
          next_accum = val ++ accum
          next_que = Enum.reduce(val, rest, fn key, que -> :queue.in(graph[key], que) end)
          _bfs(graph, target, next_que, next_accum)
        end
      {:empty, _} -> {:not_found}
    end
  end
end

簡易テストを実行

tests = [
  Algo.bfs(graph, :okb, :f) == {:exist, :f},
  Algo.bfs(graph, :okb, :h) == {:not_found},
  Algo.bfs(graph, :b, :f) == {:not_found},
  Algo.bfs(graph, :c, :f) == {:exist, :f}
]

IO.inspect(tests)
#result:
#[true, true, true, true]

queueの使い方を色々、試すのに時間がかかった
Elixirでのデータの動かし方についてはかなりマシになってきたとは思ってます(自己満
もっとこうした方が良いよなどありましたら教えてください

【実装コード有り】アルゴリズム初心者がセレクトソートとクイックソートをElixirで実装する

ソートの必要性について

www.okb-shelf.work

前回の記事で実装した二分探索は対象のリストがソートされていることが前提であり、つまりはリストをソートしてくれるアルゴリズムが必要になるということ
え?もうEnum.sort()あるけど?とは言わない約束
一度自分で実装することが大事なんじゃないかと勝手に思ってます

セレクトソートについて

超シンプルなソート方法です
リストから最小値を取得して新たな配列に順次追加してくというもの
シンプルなリストを使って実際の動きを見てみる

簡単な例

lst = [7,3,2,5,4,8,9,1,10,6]
sorted_lst = []

#1回目
min_num = 1
lst = [7,3,2,5,4,8,9,10,6]
sorted_lst = [1]

#2回目
min_num = 2
lst = [7,3,2,5,4,8,9,10,6]
sorted_lst = [1,2]
:
:
#10回目
min_num = 10
lst = []
sorted_num = [1,2,3,4,5,6,7,8,9,10]

とりあえずElixirで実装してみる

Elixirでのセレクトソート
最小値の探索とベースのリストから最小値の削除には Enum.min()とList.delete()メゾットをコンボで行なっている
同じ数値が現れても問題なく操作を行える

iex(13)> min = Enum.min([1,1,2,3,4,5])
1
iex(14)> List.delete([1,1,2,3,4,5], min)
[1, 2, 3, 4, 5]

こいつを使ってセレクトソートを実装するとこんな感じになった

defmodule Algo do
  def select_sort(lst) do
    _select_sort(lst, [])
  end
  defp _select_sort([], accum), do: accum
  defp _select_sort(lst, accum) do
    min_num = lst |> Enum.min()
    next_lst = List.delete(lst, min_num)
    _select_sort(next_lst, accum ++ [min_num])
  end
end

正しく動作しているかを簡易テストで確かめる

tests = [
  Algo.select_sort([1,4,3,7,5,1,2,13,9,23,5,57]) == [1, 1, 2, 3, 4, 5, 5, 7, 9, 13, 23, 57],
  Algo.select_sort([]) == [],
  Algo.select_sort([1]) == [1],
  Algo.select_sort([2,4,3,5]) == [2,3,4,5],
  Algo.select_sort([4,2,6,7,8,3,12,1,3,76,345,0,67]) == [0, 1, 2, 3, 3, 4, 6, 7, 8, 12, 67, 76, 345]
]

IO.inspect(tests)
#[true, true, true, true, true]

いい感じですね
ただ、この方式だとリストの要素数だけ上記の試行を行う必要がある
そのため10億個の要素があるようなリストが現れた日には詰む
より手順の少ないソートが必要になる

クイックソートについて

言葉で説明するのが難しいので実データの動きを見ていく
先ほど同様にシンプルなリストを用意した

lst = [7,3,2,5,4,8,9,1,10,6]

ではソートを開始
lstの先頭の値を取得してsorted_lstに追加していく
追加する際は昇順になるように適宜行う

#1回目
lst = [3,2,5,4,8,9,1,10,6]
sorted_lst = [7]

#2回目
lst = [2,5,4,8,9,1,10,6]
sorted_lst = [3,7]

#3回目
lst = [5,4,8,9,1,10,6]
sorted_lst = [2,3,7]

#4回目
lst = [4,8,9,1,10,6]
sorted_lst = [2,3,5,7]
:
:
#10回目
lst = [6]
sorted_lst = [1,2,3,4,5,6,7,8,9,10]

クイックソートはどのように要素を取得するかによって試行回数が大きく変化するらしい
今回はElixirのheadとtailを使いたかったので先頭の要素をマッチさせて取得している
この取得した要素のことをピポットと言い、ピポットの取得方法によって性能が変化する

Elixirでのクイックソート
quick_sort()とappend()でそれぞれ再帰している
quick_sort()では受け取ったリストから先頭を取得してソート済み配列に追加していく
追加の際には昇順になるように追加したいのでappend()で判定しつつ良しなに追加する

defmodule Algo do
  def quick_sort(lst), do: _quick_sort(lst, [])
  defp _quick_sort([], accum), do: accum
  defp _quick_sort([head | tail], accum) do
    res = append(head, accum, [])
    _quick_sort(tail, res)
  end
  def append(num, [], accum), do: accum ++ [num]
  def append(num, [head | tail], accum) do
    case num > head do
      true -> append(num, tail, accum ++ [head])
      false -> accum ++ [num] ++ [head] ++ tail
    end
  end
end

同様にテストを試す

tests = [
  Algo.quick_sort([1,4,3,7,5,1,2,13,9,23,5,57]) == [1, 1, 2, 3, 4, 5, 5, 7, 9, 13, 23, 57],
  Algo.quick_sort([]) == [],
  Algo.quick_sort([1]) == [1],
  Algo.quick_sort([2,4,3,5]) == [2,3,4,5],
  Algo.quick_sort([4,2,6,7,8,3,12,1,3,76,345,0,67]) == [0, 1, 2, 3, 3, 4, 6, 7, 8, 12, 67, 76, 345]
]

IO.inspect(tests)
#result
#[true, true, true, true, true]

もう少し短く早くできたのでこちらもどうぞ
たぶんこれが一番早いと思います(大嘘

defmodule Algo do
  def quick_sort(lst) when length(lst) < 2, do: lst
  def quick_sort([head | tail]) do
    smaller = Enum.filter(tail, &(&1 <= head))
    bigger = Enum.filter(tail, &(&1 > head))
    quick_sort(smaller) ++ [head] ++ quick_sort(bigger)
  end
end

二分探索のように

  • ピポット以上の値を持つリスト
  • ピポット以下の値を持つリスト

の2つに分割してソートを行なっていく

lst = [3,2,5,4,8,9,1,10,6]
fetch_num = 3
smaller = [2,1]
bigger = [5,4,8,9,10,6]

quick_sort(smaller) ++ [fetch_num] ++ quick_sort(bigger)
#最終形はこんな感じになる
[1] ++ [2] ++ [3] ++ [4] ..... [10]

また簡易テストを試す

tests = [
  Algo.quick_sort([1,4,3,7,5,1,2,13,9,23,5,57]) == [1, 1, 2, 3, 4, 5, 5, 7, 9, 13, 23, 57],
  Algo.quick_sort([]) == [],
  Algo.quick_sort([1]) == [1],
  Algo.quick_sort([2,4,3,5]) == [2,3,4,5],
  Algo.quick_sort([4,2,6,7,8,3,12,1,3,76,345,0,67]) == [0, 1, 2, 3, 3, 4, 6, 7, 8, 12, 67, 76, 345]
]

IO.inspect(tests)
#result
#[true, true, true, true, true]

いい感じですね
こういう書き方をスパッと思いつける人間になりたいです

【実装コード有り】アルゴリズム初心者がElixirで二分探索のコードを実装するまで

二分探索の生まれた背景

昇順ソート済みのリスト(配列)から特定の値のindex番号を取得したいとする

#8のindex番号はいくつ?(7が知りたい)
item = 8
lst = [1,2,3,4,5,6,7,8,9,10]

これを単純に配列の頭から探索していくと
index番号の0から初めて7番目、すなわち8回目の試行で特定のindex番号を取得することができる
ここまで一見、何も問題が無いように思えるが、以下の場合を考えてみる

lst = [1,2,3,4, .... 10000000]
item = 9999999

先ほどのように単純に配列の頭から探索をしたとすると最低でも9999999の試行が必要になる
あーなら、後ろから探索すればええんじゃないの?って思いますよね
後ろから探索した場合は2回の試行で終了です。やりました

では5000000を探したい場合はどうなるのか
頭からでも後ろからでも最低でも5000000の試行が必要になる

うわー、効率悪いね...となり生まれたのが二分探索です

二分探索の動き方

先ほども書いた通りにリストは昇順ソートされているとする
今回は

item = 3
lst = [1,2,3,4,5,6,7,8,9,10]

という条件で探索を行う
f:id:takamizawa46:20190430095620j:plain:w450

二分探索ではその名の通り、対象のリストを中心から2つに分割する
中心値を取得してそれ以下の数値をlst1に
中心値より大きな数値をlst2に分割する
今回の場合は少数は切り捨てられて以下のようになる

lst = [1,2,3,4,5,6,7,8,9,10]
fetch_center_num = 5
lst1 = [1,2,3,4]
lst2 = [6,7,8,9,10]

また取得した中心値が今回探索したい値(3)と等しいかをチェックする
f:id:takamizawa46:20190430095647j:plain:w450

今回の探索したい値(3)は5より小さいため、5以上の数値をもつlst2を破棄する
よって次はlst1に対して探索を行う
f:id:takamizawa46:20190430095713j:plain:w450

lst = [1,2,3,4]
fetch_center_num = 2
lst1 = [1]
lst2 = [3,4]

それでまた中心値を取得して同様に比較する
今回の中心値(2)は(3)より小さいため、2以下のの数値をもつlst1を破棄する

f:id:takamizawa46:20190430095742j:plain:w450

さらに同じことを繰り返す
f:id:takamizawa46:20190430095816j:plain:w450

中心値を取得して比較をする
今回の中心値が探索したい値と一致したため、試行を終了して
index番号を返す
f:id:takamizawa46:20190430095853j:plain:w450 f:id:takamizawa46:20190430095920j:plain:w450

見事(3)を見つけることが出来ました
この時点での試行回数は3回なので単純に探索するのとほぼ同じ試行回数となりました

二分探索での試行回数はn個の要素を持つリストの場合に log2 nとなります
つまりはn = 2 * numのnumを求めればOK

  • 64個の要素をもつリストであれば6回
  • 128 個の要素をもつリストであれば7回
  • 1024個の要素をもつリストであれば10回

と要素が2倍になっても少ない回数で試行を終了することが可能

Elixirで書いてみる

とりあえず動くものが完成した
まずはbinary_search()をヘルパーとして使ってリストの長さを_binary_search()に渡している
あとは成否判定をプライベート関数のjudgeにやらせる

defmodule Algorithm do
  def binary_search(lst, item) do
    _binary_search(lst, 0, Enum.count(lst) -1, item)
  end
  defp _binary_search(lst, low, high, item) do
    mid = div(low + high, 2)
    value = Enum.at(lst, mid)
    if mid == Enum.count(lst) -1 and value != item do
        :not_found
    else
        judger(lst, low, high, item, mid, value)
    end
  end
    
  defp judger(_lst, _low, _high, item, mid, value) when value == item, do: mid
  defp judger(lst, low, _high, item, mid, value) when value > item, do: _binary_search(lst, low, mid-1, item)
  defp judger(lst, _low, high, item, mid, value) when value < item, do: _binary_search(lst, mid+1, high, item)
end

res = Algorithm.binary_search([1,3,4,5,6,7,23,56, 244,456], 5336)
IO.puts(res) #not_found

しかし、bugを発見してしまった
これだと存在しない値をうまく発見できない場合がある
0や-4のようにリストの先頭値より小さな値を探索する場合に終了せずに無限ループしてしまう

簡易テストの結果

tests = [
    Algorithm.binary_search([1,2,3,4,5,6,7,8,9,10], 1) == 0,
    Algorithm.binary_search([1,2,3,4,5,6,7,8,9,10], 3) == 2,
    Algorithm.binary_search([1,2,3,4,5,6,7,8,9,10], 10) == 9,
    Algorithm.binary_search([1,2,3,4,5,6,7,8,9,10], 15) == :error,
    Algorithm.binary_search([1,2,3,4,5,6,8,9,10], 7) == :error,
    Algorithm.binary_search([1,2,3,4,5,6,7,8,9,10], -10) == :error
]

IO.inspect(tests) #どこかで無限ループして結果が表示されない

あと単純にコードがごちゃごちゃしてて可読性が悪い
ので、以下のように修正

defmodule Algorithm do
  def binary_search(lst, item) do
    _binary_search(lst, 0, Enum.count(lst) -1, item)
  end
  defp _binary_search(lst, low, high, item) do
    mid = div(low + high, 2)
    value = Enum.at(lst, mid)
    cond do
        value == item -> mid
        low == high -> :error
        value > item -> _binary_search(lst, low, mid-1, item)
        value < item -> _binary_search(lst, mid+1, high, item)
    end
  end
end

発見できない場合の終了条件に以下を設定した

low == high

以下の場合を考えてみる

item = 999
lst = [1,2,3,4,5,6,7,8,9,10]
回数 low high
1回目 0 10
2回目 5+1 10
3回目 (10+5+1)/2 10
4回目 (8+1+10)/2 10
5回目 (9+1+10)/2 10

--> low == high になった

思った挙動をしているか再びテストをしておく

tests = [
    Algorithm.binary_search([1,2,3,4,5,6,7,8,9,10], 1) == 0,
    Algorithm.binary_search([1,2,3,4,5,6,7,8,9,10], 3) == 2,
    Algorithm.binary_search([1,2,3,4,5,6,7,8,9,10], 10) == 9,
    Algorithm.binary_search([1,2,3,4,5,6,7,8,9,10], 15) == :error,
    Algorithm.binary_search([1,2,3,4,5,6,8,9,10], 7) == :error,
    Algorithm.binary_search([1,2,3,4,5,6,7,8,9,10], -10) == :error
]

IO.inspect(tests)
#result:
#[true, true, true, true, true, true]

よさそうですね 最近、アルゴリズム組むの下手くそだなって思って勉強してますが
このコード書くだけで4時間ぐらいかかってしまった
pythonだとわりとすんなり書けるけど再帰だと頭をめちゃくちゃ使う

もっと短く出来るや、ここダメなんじゃね?という点がありましたら
ご指摘ください

【サンプルコード有り】ElixirでTaskを使った簡単な並行処理の実装方法

Taskとは

Elixirでプロセスを作成する方法はいろいろあります
以前はelixirで並列処理を使ってファイルを同時に開き特定の文字を検索する
並列処理をやりましたが、その時は

spawn(module, :func, [argument)

こんな感じでspawnメゾットを使ってプロセスを生成した

defmodule Sample do
  def hello do
    IO.puts("hello world")
  end
end

pid = spawn(Sample, :hello, [])
#なにかしらのプロセス値が表示される
iex> pid
#PID<0.1873.0>

#現在のプロセス値
iex> self()
#PID<0.196.0>

こんな感じでプロセスが生成されているのが分かる

ただ毎度、シンプルな関数のためにプロセスを生成してメッセージを送信して受信して...
なんてのは単純にしんどいしコード量も増えて疲れる(ゲッソリ

そこでElixirにはTaskというプロセスの生成が可能な
クソ便利なものが用意されているわけです
シンプルな関数を使用した並行処理がめちゃくちゃ簡単に書くことができる

利用のしやすさ

spawnとmessage << Task

Taskの使い方

まずはTaskに投げる用のサンプルモジュールを作成しておく
ただのサンプルなのでアホほどシンプルに。メゾットは引数同士を足し合わせるadd()のみ

defmodule Sample do
  def add(x, y) do
    x + y
  end
end

ではさっそくTaskを使ってプロセスを生成してみる
Task__Task.start()__を使用する
引数についてはspawnと全く同じだと思ってください

res = Task.start(Sample, :add, [1,2])
iex> res
{:ok, #PID<0.1929.0>}

先ほどのspawnメゾットと異なり戻り値はTask構造体になっている
プロセスの生成に成功した場合にのみ{:ok, pid}という形式で値が返ってくる

しかし、今の所Task.start()を使って並列処理をしたことはありません
もっと便利な__Task.async()__というメゾットがあります

task = Task.async(Sample, :add, [1,2])
%Task{
  owner: #PID<0.196.0>,
  pid: #PID<0.1956.0>,
  ref: #Reference<0.838616553.1924923393.12356>
}

Task.async()でプロセスの生成に成功するとTask構造体に含まれた以下の値が返ってくる

  • 現在のプロセスpid
  • 生成されたプロセスpid
  • よくわからんやつ -> spawn_linkで生成される値

この時点ではバックラウンドで処理が走っただけで結果の値は取得できていない
結果の値を取得するためにはTask.await()を使う

Task.await(task)
3

この時点で生成したプロセスは役目を終えてさよならしてるので
もう一度、Task.await(task)をcallするとerrorになる

Task.await(task)
** (exit) exited in: Task.await(%Task{owner: #PID<0.196.0>, pid: #PID<0.1989.0>, ref: #Reference<0.838616553.1924923393.12399>}, 5000)
    ** (EXIT) time out
    (elixir) lib/task.ex:577: Task.await/2

もしくはTask.yield(task, timeout_second)というメゾットを使うこともできる
第2引数に渡したsecond分の時間が経過してもタスクが終了しない場合にはnilを自動的に返してくれる

#再度プロセスを生成する
task = Task.async(Sample, :add, [1,2])

iex> Task.yield(task, 1000)
{:ok, 3}

#もうプロセスは消滅しているがyieldだとerrorにならない
#3秒だけ様子見。見つからなかったようだ
iex(55)> Task.yield(task, 3000)
nil

もう少し応用的な使い方

基礎の知識が身についたところでもう少し複雑な処理を行ってみる
まずは複数個の列挙可能データ(今回はレンジ)を用意して、1つのリストに格納する(リストinレンジ)
サイズは適当でok

data = [
  1..100,
  1..101,
  1..101,
  :
]

このdata内の各レンジに対してそれぞれプロセスを生成して
各要素を2倍した後に合計した物をreturnするという処理をTaskで行ってみる

まずは元となるモジュールと関数を用意

defmodule TaskPractice do
  def lst_adjudtment(lst) do
    lst
      |> Enum.map(&(&1 * 2))
      |> Enum.sum()
  end
end

この部分に関しては今更なので特に触れることはないかと
次に良しなにリストinレンジを用意する

iex> lists = Enum.reduce(1..10, [], fn x, acc -> acc ++ [1..x+100] end)
[1..101, 1..102, 1..103, 1..104, 1..105, 1..106,
 1..107, 1..108, 1..109, 1..110]

あとはいつものようにEnum.map()を使えば上手く出来そう
リストinタスクたるものが出来上がる

#各要素のレンジをTask.asyncの第3引数に投げる(要素の数だけプロセスが生成される)
iex> tasks = Enum.map(lists, &(Task.async(TaskPractice, :lst_adjudtment, [&1])))
[
  %Task{
    owner: #PID<0.200.0>,
    pid: #PID<0.216.0>,
    ref: #Reference<0.640011924.2942042115.104787>
  },
  %Task{
    owner: #PID<0.200.0>,
    pid: #PID<0.217.0>,
    ref: #Reference<0.640011924.2942042115.104788>
 },
  :
  :
  %Task{
    owner: #PID<0.200.0>,
    pid: #PID<0.225.0>,
    ref: #Reference<0.640011924.2942042115.104796>
  }
]

あとはTask.awaitを使って値を取得すればok
ついでにパイプ使ってクールにまとめておく

iex> res = Enum.reduce(1..10, [], fn x, acc -> acc ++ [1..x+100] end)
          |> Enum.map(&(Task.async(TaskPractice, :lst_adjudtment, [&1])))
          |> Enum.map(&(Task.await(&1)))

[10302, 10506, 10712, 10920, 11130, 11342, 11556, 11772, 11990, 12210]

上手く行きましたね
以前はあれほどspawnで苦労した並列処理が

  • Task.asyncでモジュールと関数と引数指定
  • Task.awaitで値取得

のたった2ステップで完結しました。やったね
ちなみにですが、Task.async()には関数だけを投げることも可能です

iex> task = Task.async(fn -> 3+3 end) 
%Task{
  owner: #PID<0.200.0>,
  pid: #PID<0.239.0>,
  ref: #Reference<0.640011924.2942042113.105978>
}

iex> Task.await(task)
6

おまけのコーナー

いつぞやにつくった公開APIをcallするモジュールのメゾットを並列処理で動かしてみる

defmodule CallApi do
  def fetch_ghibli_films() do
    HTTPoison.get!("https://ghibliapi.herokuapp.com/films").body
      |> Poison.Parser.parse!()
      |> Enum.filter(&(&1["director"] == "Hayao Miyazaki"))
      |> Enum.map(&(&1["title"]))
  end
end
try_total = 5 
res = 1..try_total
  |> Enum.map(fn _ -> Task.async(CallApi, :fetch_ghibli_films, []) end)
  |> Enum.map(&(Task.await(&1)))

res
[
  ["Castle in the Sky", "My Neighbor Totoro",
   "Kiki's Delivery Service", "Porco Rosso",
   "Princess Mononoke", "Spirited Away",
   "Howl's Moving Castle", "Ponyo",
   "The Wind Rises"],
  ["Castle in the Sky", "My Neighbor Totoro",
   "Kiki's Delivery Service", "Porco Rosso",
   "Princess Mononoke", "Spirited Away",
   "Howl's Moving Castle", "Ponyo",
   "The Wind Rises"],
  :
  :
  ["Castle in the Sky", "My Neighbor Totoro",
   "Kiki's Delivery Service", "Porco Rosso",
   "Princess Mononoke", "Spirited Away",
   "Howl's Moving Castle", "Ponyo",
   "The Wind Rises"]
]

秒で取得完了。えぐい

【レポート】第3回清流elixir勉強会in丸の内を開催しました【マップのパターンマッチ】

トピック

昨日、愛知県の丸の内にて私が運営しているコミュニティ清流elixir
第3回目となる勉強会を開催させて頂きました
さすがに3回目ともなると多少は要領が分かってきてわりとスマートに
活動できるようになってきたかと思ってます(力不足ですません

清流elixir-infomation
開催場所: 丸の内(愛知)
人数: 2 -> 3
コミュニティ参加人数 : 3 -> 5 update!!
20190427現在

第3回の活動内容

今回は前回の勉強会中につまづいたマップのパターンマッチについて学習しました
どうすればマップから指定のkeyを持つデータをマッチできるのかをいろいろ試した

f:id:takamizawa46:20190427100226j:plain:w450
マップから値を取得(マッチ)するには...?

ただElixirにはマップの記述方法が2種類あり、それぞれで取得の仕方が微妙に異なるので注意
それぞれに名称も特になく勝手に命名しておく

  • Atomを使用する楽な記述(Atom式と命名)
  • 普通の書き方%{"name" => "okb"}(温故知新スタイルと命名)

f:id:takamizawa46:20190427100233j:plain
アトム式と温故知新スタイル

Rubyにも同様に2種類の記述方法があるようで
それは歴史的な流れでより簡単に記述するためにということで
Elixirでいうアトム式が誕生したとのこと
使い分けについて議論しましたが完璧な答えは出なかった
温故知新スタイルでは記号でも日本語でも何でも(バイナリ)をkeyにできるが
それにメリットがあるかは分からなかった

どちらを使うにしろチームで統一して使えばいいんじゃね?という結論になりました

Atom式からのマッチング

このマップデータから:nameのkeyを様々な方法でマッチしてみる

info = %{name: "nobunaga", age:45}

1.最もシンプルなマッチ

info[:name]  #nobunaga

#この書き方はerror
info[name] #error

#存在しないkeyをマッチさせようとしてもerrorにはならない
info[:company] #nil

2.次にシンプルなマッチ

info.name #nobunaga

#存在しないkeyをマッチさせようとするとerror
info.company #error

3.keyの存在を確認しつつのマッチ

#%{key_name: variable_name} = data
%{name: name} = info
name #nobunaga

#変数名は自由(アンダースコアはNG)
%{name: human_name} = info
human_name #nobunaga

4.Map.getを使う

Map.get(info, :name) #nobunaga

#第2引数にはAtomで渡す必要がある(errorにはならない)
Map.get(info, "name") #nil

5.Map.fetchを使う

#マッチに成功した場合に{:ok, value}のタプルで返ってくる
Map.fetch(info, :name) # {:ok, value}
{judge, name} = Map.fetch(info, :name)
name #nobunage

#存在しないkeyを指定した場合は:errorのみが返ってくる
Map.fetch(info, :company) # :error

f:id:takamizawa46:20190427100254j:plain:w450

温故知新スタイルからの取得

info = %{"name" => "nobunaga", "age" => 45}

同様にAtom式の5つのやり方を試してみる

1.最もシンプルなマッチ

info["name"] #nobunaga
info[:name] #nil

2.次にシンプルなマッチ
実はAtom式でないとこれは使えない

info."name" #error

#警告がでる上に失敗する
warning: found quoted call "name" but the quotes are not required. Calls made exclusively of Unicode letters, numbers, and underscore do not require quotes
  iex:40

** (KeyError) key :name not found in: %{"name" => "nobunaga"}

3.keyの存在を確認しつつのマッチ

#記述の仕方がAtom式と異なるので注意
%{"name" => n} = info 
n #nobunaga

#存在しないとやはりerror
%{"company" => c} = info #error

4.Map.getを使う

#第2引数にはkeyと同様の型で渡す(今回はstring)
Map.get(info, "name") #nobunaga

#errorにはならない
Map.get(info, name)

5.Map.fetchを使う

Map.fetch(info, "name") #{:ok, "nobunaga"}

#やっぱり存在しないと:error
Map.fetch(info, "company") # :error

基礎を踏まえてデータで遊ぶ

info = %{"name" => "nobunaga", "age" => 45, "from" =>  "gifu", "style" => "bushidou"}

このデータから"name"と"from"を取得して
"name"と"from"の値を結合することに(stringの結合)

name = "nobunaga"
from = "gihu"

name <> from # nobunagegihu

温故知新スタイルであることを留意して取り組むとこんな感じになった

info["from"] <> info["name"] #nobunagegihu
Map.get(info, "from") <> Map.get(info, "name") #nobunagegihu

{a, b} = Map.fetch(info, "name")
{c, d} = Map.fetch(info, "from")
b <> d #nobunagegihu

前回のリベンジ

前回はリストinマップから値をマッチするという作業でつまづいた
これだけ知識がついたのでリベンジするかということでリストinマップの情報を用意

info = [
  %{age: 22, height: 180, name: "a"},
  %{age: 16, height: 142, name: "b"},
  %{age: 43, height: 165, name: "c"},
  %{age: 67, height: 156, name: "d"},
  %{age: 81, height: 161, name: "e"},
  %{age: 8, height: 126, name: "f"}
]

リスト内の各データへのアクセスをするためにはもちろんEnumを使う
あとは今までやってきたようにマップのkeyを使ってマッチさせるだけ

Enum.map(infos, fn row -> row.age end)
#[22, 16, 43, 67, 81, 8]

Enum.map(infos, fn row -> row[:age] end)
#[22, 16, 43, 67, 81, 8]

Enum.map(infos, &(&1.age))
#[22, 16, 43, 67, 81, 8]

Enum.map(infos, fn row -> Map.get(row, :age) end)
#[22, 16, 43, 67, 81, 8]

もう楽勝ですね
このマッチは一瞬で終わってしまった

なぜ関数型言語がいいのかって

作業を進めている中で参加者の業界20年のベテランの方から
オブジェクト言語に疲れたという話がはじまり、かなり盛り上がった

動物や車などがオブジェクトのサンプルとしてよく挙げられる
しかし、機械の操作や目に見えない概念をオブジェクトにして考えるということは 難しい上に正解がなく、人それぞれで理解しにくい場合が多いとのこと

そうすると流れの掴みやすい関数型の良さがよく分かるとのことで
「なるほどなー」と強く感銘を受けました

まとめ

今回はマップのパターンマッチについて勉強会を行いました
途中から会話が無くなり少年のように没頭する時間がおもろかった
それぞれが様々な記述を発見していく感じも良かった
僕が目指している「開催者<=>参加者」という構図が少し見えてきたがする

次回はGW明けの5/10(金)の19:30からの開催予定です
お気軽に覗きにきてください