やわらかテック

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

【実装コード有り】アルゴリズム初心者が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からの開催予定です
お気軽に覗きにきてください

【サンプルコード有り】Elixirでのhttpリクエストを簡単な叩き方

公開APIについて

以前「清流elixir」の勉強会でこの公開API、通称ジブリAPI(勝手に命名)を
Enumとパイプ演算子を使って遊ぶ予定だったんですけど
当日になってcurlの戻り値が%{}(マップ形式)となっていないことに気づく(そりゃそう

知見としてもかなり強力なのでElixirからAPIを叩けるようにしないとなーと
やってみたらめちゃくちゃ簡単でした

せっかくなので戻り値から特定のデータを抽出するところまで触れてみます

プロジェクトの準備

mixとmixを使ったプロジェクトの立ち上げについてはこの記事で詳しく触れているので省略します
いつものようにmix newコマンドを叩く

mix new call_api
cd call_api

今回の使用するライブラリは以下の2つ

  • httpoison(Httpクライアント)
  • poison(Jsonを解析するやつ)

いつものように./call_api/mix.exsのdpsに上2つを記述します
./call_api(project_name)/mix.exs

defp deps do
    [
      {:httpoison, "~> 1.4"},
      {:poison, "~> 3.1"}
    ]
end

からの

mix deps.get

でライブラリをダウンロードする
準備はこれで完了

使用するエンドポイント

ドキュメントにあるこのエンドポイントをGETでcallします

https://ghibliapi.herokuapp.com/films

こんな感じでリスト内にJson形式のデータが複数返ってくる

[
  {
    "id": "2baf70d1-42bb-4437-b551-e5fed5a87abe",
    "title": "Castle in the Sky",
    "description": "The orphan Sheeta inherited a mysterious crystal that links her to the mythical sky-kingdom of Laputa. With the help of resourceful Pazu and a 
    rollicking band of sky pirates, she makes her way to the ruins of the once-great civilization. Sheeta and Pazu must outwit the evil Muska, who plans to use 
    Laputa's science to make himself ruler of the world.",
    "director": "Hayao Miyazaki",
    "producer": "Isao Takahata",
    "release_date": "1986",
    "rt_score": "95"
  },
  {
    "id": "12cfb892-aac0-4c5b-94af-521852e46d6a",
    "title": "Grave of the Fireflies",
    "description": "In the latter part of World War II, a boy and his sister, orphaned when their mother is killed in the firebombing of Tokyo, are left to survive on their 
    own in what remains of civilian life in Japan. The plot follows this boy and his sister as they do their best to survive in the Japanese countryside, battling hunger, 
    prejudice, and pride in their own quiet, personal battle.",
    "director": "Isao Takahata",
    "producer": "Toru Hara",
    "release_date": "1988",
    "rt_score": "97"
  }
  :
  :
]

さっそくコードを書いていく

今回の目的を達成するために必要な流れはこんな感じ
1. APIをcallして戻り値を得る(Httpoison)
2. 戻り値を解析する(Poison)
3. 解析した結果から特定のデータのみを抽出する(パターンマッチ)

ここまで考えたらあとはコードに落としていくのみ
とりあえず戻り値からtitleを抽出してみる
./call_api/lib/call_api.ex

def fetch_ghibli_films() do
    #Httpoison使ってAPIをcall
    {status, res} = HTTPoison.get("https://ghibliapi.herokuapp.com/films")
    case status do
      :ok ->
        #戻り値のbody(json)を解析
        Poison.Parser.parse!(res.body)
          |> Enum.map(&(&1["title"]))
      :error -> :error
    end
end

前回のAmazonレビューのスクレイピングと異なり
今回はHttpoison.get!()を使わずにget()を使ってます
get()の戻り値には {:ok, return} もしくは {:error reason}という値が返ってくるので
APIのcallの成否をstatusのアトム値をcase文使って分岐させています

この時点でresにはこんなような値が入っている

%HTTPoison.Response{
  body: "[\n  {\n    ...,
  headers: [
    {"Server", "Cowboy"},
    :
  ],
  request: %HTTPoison.Request{
    body: "",
    headers: [],
    method: :get,
    options: [],
    params: %{},
    url: "https://ghibliapi.herokuapp.com/films"
  },
  request_url: "https://ghibliapi.herokuapp.com/films",
  status_code: 200
}

bodyの値(json)をPoison使って解析します
これでようやくElixirで扱える形になった

Poison.Parser.parse!(res.body)

[
  %{
    "description" => "The orphan...",
    "director" => "Hayao Miyazaki",
    "id" => "2baf70d1-42bb-4437-b551-e5fed5a87abe",
    "locations" => ["https://ghibliapi.herokuapp.com/locations/"],
    "people" => ["https://ghibliapi.herokuapp.com/people/"],
    "producer" => "Isao Takahata",
    "release_date" => "1986",
    "rt_score" => "95",
    "species" => ["https://ghibliapi.herokuapp.com/species/af3910a6-429f-4c74-9ad5-dfe1c4aa04f2"],
    "title" => "Castle in the Sky",
    "url" => "https://ghibliapi.herokuapp.com/films/2baf70d1-42bb-4437-b551-e5fed5a87abe",
    "vehicles" => ["https://ghibliapi.herokuapp.com/vehicles/"]
  },
  :
  :
]

マップのパターンマッチについて

あとは上記のリストinマップからtitleの値をどうやってパターンマッチさせるかですね
思いつくのはこれぐらい

info = %{"title" => "Castle in the Sky"}

#超シンプルなやつ
info["title"] #Castle in the Sky

#これだとエラー
info[title] #値がねーよと怒られる。そりゃ当然

#エラーにはならないが戻り値が空
info[:title]
info["new_title"]
#"title"というkeyがあるか
%{"title" => title} = info #title -> Castle in the Sky

#atom形式でないからこの書き方はできない
%{"title": title} = info 

#存在しないkeyを指定するとerror
%{"new_title": title} = info #error
#Mapモジュールを使用する
Map.get(info, "title") #Castle in the Sky

#存在しないkeyをしてもerrorにはならず空
Map.get(info, "new_title")

とりあえず一番シンプルな方法を今回は選択した
あとはリスト内のマップに対して操作を適用したいのでいつも通りにEnum.map()を使えば良い

Enum.map(&(&1["title"]))

お待たせしました
ではfetch_ghibli_films() を呼び出してみる

iex(8)> CallApi.fetch_ghibli_films()
["Castle in the Sky", "Grave of the Fireflies", "My Neighbor Totoro",
 "Kiki's Delivery Service", "Only Yesterday", "Porco Rosso", "Pom Poko",
 "Whisper of the Heart", "Princess Mononoke", "My Neighbors the Yamadas",
 "Spirited Away", "The Cat Returns", "Howl's Moving Castle",
 "Tales from Earthsea", "Ponyo", "Arrietty", "From Up on Poppy Hill",
 "The Wind Rises", "The Tale of the Princess Kaguya", "When Marnie Was There"]

お、良い感じに取れてますね
無事にAPIをcallすることが出来た上に、簡単な解析まで完了しました

実はもっと頑張ると3行で書くことが出来ます

def fetch_ghibli_films() do
    HTTPoison.get!("https://ghibliapi.herokuapp.com/films").body
      |> Poison.Parser.parse!()
      |> Enum.map(&(&1["title"]))
end

おまけのコーナー

directorが"Hayao Miyazaki"である作品のtitleを抽出してみる

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

#result
#["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"]

この辺りの操作はやっぱりElixirは強力ですね

ErlPortでpythonのクラスメゾット呼び出しとElixir側にAtomを返す

前回のはなし

www.okb-shelf.work

Elixirからpythonの関数を呼び出して、最終的にjanomeを使って形態素解析を行いました

本当はpythonのclassメゾットの呼び出しとElixirに対して
Atomのデータを返すということがやりたかったんですけど
そこそこ記事が長くなってしまったのでカット

今回の記事で触っていく
ErlPortの公式にclassメゾットの呼び出しについてほとんど記述が無かったので割としんどかった

python側での準備

単純なclassメゾットを用意

class EasyClassMethod:
  def output_msg(self, msg):
    decode_msg = msg.decode("utf-8")
    print(decode_msg)
    return True

classなのにただ文字列を出力するだけのカス
文字列のdecodeについては前回の記事で触れてるのでどうぞ

Elixir側での準備

ほとんど前の記事のものと同じで異なるのは:python.callを使用して
クラスのインスタンスをさりげなく作っているところです

demodule CallPython do
  def call_esay_method(msg) do
    {:ok, py_processs} = :python.start([python_path: 'python_files'])
    #EasyClassMethodのインスタンスを作ってる(object)
    obj = :python.call(py_processs, :test_call, :EasyClassMethod, [])
    #呼び出す関数はアトムの文字列で与える必要あり,引数の第1にobjectを渡す
    res = :python.call(py_processs, :test_call, :"EasyClassMethod.output_msg", [obj, msg])
    IO.puts(res)
    :python.stop(py_processs)
    :ok
  end
end

とりえあず動作を見てみると

iex(3)> CallPython.call_esay_method("hello world")
hello world
true
:ok

いい感じですね

それぞれの出力値を追っていくと
obj
謎のバイナリーデータが入っている

#objのreturn
{:"$erlport.opaque", :python,
 <<128, 2, 99, 116, 101, 115, 116, 95, 99, 97, 108, 108, 10,
   69, 97, 115, 121, 67, 108, 97, 115, 115, 77, 101, 116, 104,
   111, 100, 10, 113, 0, 41, 129, 113, 1, 46>>}

res
メゾットの戻り値が来ている。いいね

true

クラス化したJanomeParserを呼び出す

とりあえず基礎の部分は抑えたのでこの記事で作成したclassメゾットを呼び出してみる
python

class JanomeParser:
    def __init__(self):
        print("come here: init method")
        self.t = Tokenizer()
        
    def parser(self, value_str, tag=u"名詞"):
        res = self.t.tokenize(value_str)
        if isinstance(tag, list):
            return [token.surface for token in res if token.part_of_speech.split(",")[0] in tag]
        else:
            return [token.surface for token in res if token.part_of_speech.split(",")[0] == tag]

Elixir

def class_caller(value_str, tag) do
    {:ok, py_process} = :python.start([python_path: 'python_files'])
    obj = :python.call(py_process, :test_call, :JanomeParser, [])
    res = :python.call(py_process, :test_call, :"JanomeParser.parser", [obj, value_str, tag])
      |> Enum.map(&(to_string(&1)))
    :python.stop(py_process)
    res
end

今まで通り呼び出すが、実は2点問題がある
とりあえず実行結果を先にお見せします

iex(3)> CallPython.class_caller("言葉一つで足りないくらい全部壊れてしまうような", "名詞")
come here: init method
** (ErlangError) Erlang error: {:python, :"builtins.ValueError", 'unsupported data type: <class \'test_call.JanomeParser\'>',
:
:
    (call_python) lib/call_python.ex:46: CallPython.class_caller/2

どうやらinitiを呼び出しまでは問題なく行われているが

self.t = Tokenizer()

ってのが上手く行かないっぽい
あとparserに渡した文字列を毎度のごとくdecodeする必要がある
ということでdecodeをしつつ、initでinstanceを生成するのを止めたら動いた

class JanomeParser(object):
  def __init__(self):
    print("come here")

  def parser(self, value_str, tag):
    def decoder(value_str, str_code="utf-8"):
      return value_str.decode(str_code)

    tag = PART_OF_SPPECH.get(decoder(tag))
    t = Tokenizer()
    res = t.tokenize(decoder(value_str))
    if isinstance(tag, list):
      return [token.surface for token in res if token.part_of_speech.split(",")[0] in tag]
    else:
      return [token.surface for token in res if token.part_of_speech.split(",")[0] == tag]
iex(5)> CallPython.class_caller("言葉一つで足りないくらい全部壊れてしまうような", "名詞")
come here
["言葉", "一つ", "全部", "よう"]

いい感じだけど、class化した意味があんまりないのがつらい
外部でインスタンス作ってinitに渡してあげればいいかもなと思い試したけどダメでした(デデドン

class JanomeParser(object):
  def __init__(self, tokenizer):
    print("come here")
    self.t = tokenizer

  def parser(self, value_str, tag):
    def decoder(value_str, str_code="utf-8"):
      return value_str.decode(str_code)

    tag = PART_OF_SPPECH.get(decoder(tag))
    res = self.t.tokenize(decoder(value_str))
    if isinstance(tag, list):
      return [token.surface for token in res if token.part_of_speech.split(",")[0] in tag]
    else:
      return [token.surface for token in res if token.part_of_speech.split(",")[0] == tag]


def create_tokenizer_instance():
  return Tokenizer()
def class_caller(value_str, tag) do
    {:ok, py_process} = :python.start([python_path: 'python_files'])
    tokenizer = :python.call(py_process, :test_call, :create_tokenizer_instance, [])
    obj = :python.call(py_process, :test_call, :JanomeParser, [tokenizer])
    res = :python.call(py_process, :test_call, :"JanomeParser.parser", [obj, value_str, tag])
      |> Enum.map(&(to_string(&1)))
    :python.stop(py_process)
    res
end

実行結果

iex(8)> CallPython.class_caller("言葉一つで足りないくらい全部壊れてしまうような", "名詞")
come here: init method
** (ErlangError) Erlang error: {:python, :"builtins.ValueError", 'unsupported data type: <class \'test_call.JanomeParser\'>',
:
:
    (call_python) lib/call_python.ex:46: CallPython.class_caller/2

さっきと同じエラーが。要研究ですね

pythonからElixirにAtomを返す

python側の関数から

:ok
{:ok, value}
{:error, reason}

のようなAtomを返すようなことがしたい場合があるかもしれない
実際にElixirでパターンマッチする際にはAtomの:okやら:errorをよくマッチさせるので使い道は多そう

Elixir側でやることはほとんどなくpython側でのごちゃごちゃが必要になる
公式ドキュメントをみると細々と記述されていた

まずは「erlport」をpython側でインストールする

pip install erlport
pip3 install erlport

こうすることでAtom型をpythonで作成可能となる

from erlport.erlterms import Atom

def return_atom_to_elixir():
  #Atomメゾットには文字列を与える
  return Atom("ok")

あとはこいつをElixirから呼び出すだけなのでいつも通りですね
Elixirとpythonの型の対応は公式ドキュメントの下の方に記述されています
ほとんどの型はそのまま受け渡しできますが

に関してはこのように関数を使って定義する必要があります
他にもpython側のNoneはElixir側ではundefinedになるなど多少異なる点があるので注意
いまさらですがErlangでは文字列はなく全てbinaryとして扱われるのでpython側でdecode, encodeが必要です

感想

classメゾットの呼び出しで結構つまづいた
もっとスッキリ使えればと思う一方、ErlPortの仕組みが気になる

そういえばErlang作者の1人であるJoe Armstrong's さんが亡くなったそうです
ElixirがあるのはErlangのおかげ。ErlangがあるのはJoe Armstrong'sのおかげです
残念です。少しでも多くの人にElixirを触ってもらえるようにアウトプット頑張ります