やわらかテック

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

【超簡単】Elixirとtrotを使って爆速でAPIを立ち上げるまで

おなじみgit探検隊

Elixirに限ったことではないが、定期的にgitでトレンドのレポジトリはチェックするようにしている
そうすると大体、何が流行っていて何に注目が集まっているかが何となく分かる
最近は中国語のREAD.MEが多くて翻訳ないと詰む

さておき、また今回も良さげなElixirのレポジトリを発見した

github.com
hexdocs.pm

An Elixir web micro-framework

説明の通り、ウェブのマイクロフレームワーク
開発自体は3年程前からされているようでラストコミットは2018年の7月っぽい

ちょうどElixirでAPIをさくっと作りたかったのでありがたい

なぜPhoenixで作らないのか

信じてもらえるか分からないが実はPhoenixでjsonAPIを作ったことはある
普通に楽々作れるし、ドキュメントもある

じゃあ、なんでtrotを使うんだって話ですけど
単に作業量が圧倒的に少ないからに限る

cloud functionやらpythonのresponderやらトレンドはミニマムに高速で作ることだと思っている
Phoenixは1つエンドポイントを作るだけとかマイクロサービスを作る際にはオーバースペック感がある
そんな機能は別に使わへんがな... あとは学習コストの問題も

だったらさくっと覚えられて素早く使える物がいいよねってことでこうなった(事後
色々とエラーには遭遇してかなり時間は取られたことは内緒

プロジェクトの新規作成

いつも通りです

mix new simple_api

./simple_api/mix.exs に以下を追加
trotのREAD.MEにはtrotだけが記述されているが、僕の環境ではerrorになったので
plug_cowboyも追加している
fakerに関しては後に使うためインストールするようにしているが無くても問題ない

def application do
    [
      extra_applications: [:logger],
      applications: [:trot, :faker]
    ]
  end

defp deps do
    [
      {:trot, github: "hexedpackets/trot"},
      {:plug_cowboy, "~> 1.0"},
      {:faker, "~> 0.12"},
    ]
  end

plug_cowboyをインストールするようにしていなかった時のerror

#mix trot.serverでerrorになってしまう

warning: please add the following dependency to your mix.exs:

    {:plug_cowboy, "~> 1.0"}

次に./sample_api/config/config.exs に以下を追加
公式ドキュメントだと割とひっそり書かれていて気づかない場合があるかもしれないが
少なくともrouterを設定するモジュールを記述しないとnot foundをただ返すだけの
ポンコツサーバーが出来上がる

config :trot, :port, 4000 #どちらでもok(デフォルトが4000番)
config :trot, :router, SimpleApi #使用するモジュールを指定(必須)

これで準備は完了

APIをさくっと作る

今回は特にdbを使っていないので触れるのは「GETとPOST」メゾットのみ
データのやり取りが出来れば十分
公式では

  • GET
  • POST
  • PUT
  • PATCH
  • DELETE
  • OPTIONS

をサポートしている
まずはGETから書いてみる

モジュールの頭に以下を追加

use Trot.Router

このRouterを記述したモジュールが先ほどconfigに記述したモジュール名と等しくなるようにする
./simple_api/lib/simple_api.ex

defmodule SimpleApi do
  use Trot.Router
  get "/easy-get" do
    "what's up!!"
  end
end

たったこれだけ
マクロになっていてサクッと記述できる
第2引数でheader情報にマッチが取れるらしいが今の所使い道は思いつかない
さっそくターミナルからcurlしてみる

❯  curl http://localhost:4000/easy-get
what's up!!%

いいですね。無事に文字列が帰ってきてる
最後に%が付くのはデフォルト? なぜだろう

次にPOST
値の受け取り方に関して公式ドキュメントに一切の記述が無くて不親切だなと思った
色々と調べた結果、Plugの関数を使うことで取得できることが分かったが
かなりここに時間をとられた(1日調べまくった
そもそもconnという謎の変数の存在に気付けへんわ
ここにリクエスト情報が詰まっているっぽい

#bodyの{"user": ___ }を取得する
user = conn.body_params["user"]
#connの中身
%Plug.Conn{
  adapter: {Plug.Adapters.Cowboy.Conn, :...},
  assigns: %{},
  :
  :
  host: "localhost",
  method: "POST",
  owner: #PID<0.369.0>,
  params: %{"user" => "okb"},
  path_info: ["easy-post"],
  path_params: %{},
  port: 4000,
  :
  :
  scheme: :http,
  script_name: [],
  secret_key_base: nil,
  state: :unset,
  status: nil
}

あとは簡単
普通に書くだけ(日本語難しい

post "/easy-post" do
    user = conn.body_params["user"]
    res = "hello, #{user}"
    IO.puts(res)
    res
end

呼び出す

curl -H "Content-Type: application/json" -X POST -d '{"user": "okb"}' http://localhost:4000/easy-post
hello, okb%

無事に値が返ってきた
ついでにサーバーサイドのログにも"hello, okb"が出力されている

基本的な部分に触れたところでもう少し便利なAPIを作ってみる

他のモジュール関数を呼び出す

せっかくなので過去に作成したモジュールを使用する
まずはGETメゾットが叩かれた時にmockデータを返すようなAPIを作る(ずっとほしかった
実装にFakerというライブラリを使っている

詳しくは以下をご覧ください

www.okb-shelf.work

今回は試しがてらURLから値を受け取る(queryではない)をやってみる

URLに渡した指定数分だけデータを返すようにしている

defmodule CreateMock do
  def user_info(num) do
    1..num |> Enum.map(fn _ -> _create_user_info() end)
  end
  defp _create_user_info do
    %{
      email: Faker.Internet.email(),
      city: Faker.Address.En.city(),
      name: Faker.Name.En.first_name() <> " " <> Faker.Name.En.last_name(),
      phone: Faker.Phone.EnUs.phone(),
      food: Faker.Food.dish(),
      age: trunc(:rand.uniform() * 100) #Erlangモジュールを使って乱数を生成
    }
  end
end

get "/mock/:create_num" do
    #受け取った時点では文字列なので注意
    info = CreateMock.user_info(String.to_integer(create_num))
    %{data: info}
  end

呼び出してみる

 ❯  curl http://localhost:4000/mock/2
{"data":[{"phone":"(988) 898-5220","name":"Hilario Dach","food":"Scotch eggs","email":"wallace.emmerich@spinka.info","city":"East Cierra","age":83},{"phone":"431-588-1937","name":"Faustino Senger","food":"Meatballs with sauce","email":"rey2086@kris.net","city":"South Friedrich","age":0}]}%

やったぜ

次にPOSTで送った2つの文字列が一致しているか、どれだけタイポしているかをチェックする
詳しくは以下をご覧ください

www.okb-shelf.work

defmodule TypoChecker do
  def main(input_val, ans_val) when input_val == ans_val, do: :good
  def main(input_val, ans_val), do: _main(String.graphemes(input_val), ans_val, 0)
  defp _main([], _, counter), do: counter
  defp _main([head | tail], ans_val, counter) do
    str_lst = String.graphemes(ans_val)
    [_n_head | n_tail] = str_lst
    case type_judge(str_lst, head) do
      :hit -> _main(tail, List.to_string(n_tail), counter+1)
      :empty -> _main(tail, List.to_string(n_tail), counter)
    end
  end

  def type_judge([], _), do: :empty
  def type_judge([head | tail], compare_str) do
    if String.contains?(compare_str, head) do
      :hit
    else
      type_judge(tail, compare_str)
    end
  end
end

post "/typo-check" do
    input_val = conn.body_params["input_val"]
    ans_val = conn.body_params["ans_val"]
    case TypoChecker.main(input_val, ans_val) do
      :good -> "no typo"
      _ -> "is typo"
    end
  end

Elixirのatomをそのままreturnするとerrorになるため
case使って無理やり文字列を返すようにしている
では呼び出す

> curl -H "Content-Type: application/json" -X POST -d '{"input_val": "apple", "ans_val": "apple"}' http://localhost:4000/typo-check
no typo%
> curl -H "Content-Type: application/json" -X POST -d '{"input_val": "apple", "ans_val": "appple"}' http://localhost:4000/typo-check
is typo%

いいですね
使い方さえ分かってしまえばtrotでのAPI作成は爆速ですわ
あとはheorkuとかにデプロイする方法を調査してまた記事にでもします

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

トピック

今回で第4回目の勉強会を僕の運営しているコミュニティで開催することができました
清流elixir

connpassでの参加人数+開催直前に新規の1名の方に参加して頂けました
少しずつ賑わってきてる感があって素直に嬉しいです

また参加者の業種や立場がバラバラであるにも関わらず
ユースケース抜きにElixirに興味がある、触ってみたいという感覚が共通しているのが非常に面白い
少なからず色んなケースで需要が見えているということなんですかね(適当

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

第4回の活動内容

いつもは僕がテーマを設定して書籍等、参照しながら
テーマに沿って説明し、余裕があれば手を動かすというスタイルでやっている

何と今回はありがたいことに初回から皆勤参加の現役学生の方が
今回の内容について全て説明をしてくれた

ありがたや...ありがたや...
もう毎回これでいいんじゃないかな(無責任

値の束縛について

Elixirにはそもそも代入という考え方はない
すべては値がマッチするかどうかを確認している
「=」はElixirではマッチ演算子と呼ばれる

a = 1 #代入ではない
a = 1 #aは1にマッチする(aは元々宣言がないため)

多くの関数型言語(Haskellやら)では値は束縛される
すなわち、再代入することが出来ない

a = 1
a = 2 #再代入あかん

全く書いたことないけど何となくHaskellで再現できた

main = putStrLn "XXXXXXXX"
sub = "aa"
sub = "bb"

{--Main.hs:5:1: error:
    Multiple declarations of ‘sub’ --}

実はElixirでは再代入(再マッチ)することが出来る
さっきも普通にaの値を更新してた
え、じゃあ束縛できへんの?となるが
「ピン演算子(^)」を使うことで可能

a = 1
^a = 2

#マッチしないと怒られる(いいね)
#** (MatchError) no match of right hand side value: 2

ピン演算子で値の不変性を保証することが出来る
チームで開発する場合にはピン演算子を使うか使わないかという共通認識を持たなければいけないと思う

何個かピン演算子で遊んでいる内に面白いことに気づいた

#errorにならない
a = 2
^a = 5 - 3 #2

^a = 4 - a #2

^a = 2 -a #errr

右側の評価が先に行われて終了した時点でマッチするかどうかを見ているっぽい
なのでこんな風に書けば先ほどerrorになったものも通る(参加者の方が発見、すご)

a = 2
(^a = 2) -a #0

()を使うことで演算の優先度の変更が変わる

色んなデータ型からのマッチ

今まで値をいかに取り出すかという視点で考えていたが
Elixri的にはいかにしてマッチさせるかという視点で考える必要があるということを
再認識して若干反省。1日1マッチを目指して進んでいく

タプル

{a, b} = {1, 2}
#a = 1
#b = 2

{a, b, c} = {1, 2}
#** (MatchError) no match of right hand side value: {1, 2}

別にこの値を使うことないわって時には「_(アンダースコア)」を使う

{a, _} = {1,2}
#a = 1
#_ error

{a, _b} = {1, 2}
#_b = 2 となるがwarningが出る(アンダースコア消せやって)

マップ
タプルのようにはマッチすることは出来ない

%{a, b} = %{name: "okb", age: 22}
#** (CompileError) iex:19: expected key-value pairs in a map, got: a

取り出しかたは5種類ほどあるが過去に説明済みなので
【レポート】第3回清流elixir勉強会in丸の内を開催しました【マップのパターンマッチ】を参照してください

関数でのマッチ

f:id:takamizawa46:20190512102647j:plain:w450
今まで触れるようで触れてなかった部分
自分のブログでも関数の宣言の仕方の解説してないのに当たり前のようにdefmoduleとか使ってて反省
時間あれば書いときます

Elixirでは関数の引数やガード節たるものを使って関数の実行前に関数でマッチすることが可能
そもそも同名の関数を同じモジュール内に宣言することが出来るのがヤバい

defmodule GreetMan do
  def greet("nobunaga", greet), do: IO.puts("#{greet}, nobunaga")
  def greet("hideyoshi", greet), do: IO.puts("#{greet}, hideyoshi")
  def greet(name, greet), do: IO.puts("#{greet}, #{name}")
end


GreetMan.greet("nobunaga", "hello") #name="nobunaga"にマッチ
GreetMan.greet("hideyoshi", "good morning") #name="hideyoshi"にマッチ
GreetMan.greet("ieyasu", "bye") #その他(?)でマッチ

# hello, nobunaga
# good morning, hideyoshi
# bye, ieyasu

ただ、これだとnobunagaとか引数渡ししてるのに
IO.putsの中で使用していたりと変更に非常に弱いコードになっている
こんな時に便利なのがガード節
以下のように関数名を宣言した後にwhenと記述するだけ

def greet(name) when name == "nobunage", do

先ほどのモジュールをwhen使って書き直すとこんな感じに

defmodule GreetMan do
  def greet(name, greet) when name == "nobunaga", do: IO.puts("#{greet}, #{name}")
  def greet(name, greet) when name == "hideyoshi", do: IO.puts("#{greet}, #{name}")
  def greet(name, greet), do: IO.puts("#{greet}, #{name}")
end


GreetMan.greet("nobunaga", "hello")
GreetMan.greet("hideyoshi", "good morning")
GreetMan.greet("ieyasu", "bye")

# hello, nobunaga
# good morning, hideyoshi
# bye, ieyasu

マッチ後の処理が全く同じなので利便性を感じづらいが関数でマッチできるのはえぐい
タプルとかのデータ構造に対しても関数の引数を使って関数実行前に値をマッチすることが可能

defmodule GreetMan do
  def is_sucess({:ok, value}), do: IO.puts(value)
  def is_sucess({:error, reason}), do: IO.puts(reason)
end

GreetMan.is_sucess({:ok, :good}) #good
GreetMan.is_sucess({:error, :bad_value}) #bad_value

再帰やるにはこの関数マッチが必須
なので流れで次回は再帰について触れることになった

次回の開催について

今回新規で参加して頂いた方のご好意で
勤務されている会社の会議室を勉強会で使わせて頂けることになりました(しかも名駅付近)
狭苦しい部屋からは脱出できそうで、本当にありがたや...ありがたや...
つながりを持つのは本当に大事ですね

いきなり広い場所になると緊張しそう
まぁたくさん間違えて恥をかいた分だけ強くなれるのでラフにいきます
内容については「Elixirでの再帰のやり方」にしようと思ってます
詳細はまた追ってconnpassで展開します

未経験者にプログラミングを教えて得られた知見と反省点

ちょっと前にプログラミングを勧めた

地元の後輩が何をしていいか分からず悩んでいるとのことで
「とりあえず損することはないからプログラミングをやっといたら?」
と責任があるのか無責任なのか分からない一言を発する

プログラミングを勧めている理由は
自身も情報学科ではなかったが独学でプログラミングを学習して世界変わったなと思っているため

これが約3ヶ月前のこと
入門しやすい

あたりをやればいいんじゃないかな?と細々と伝えたつもりが
なぜかJavaを選択(同期の友人が情報学科でJavaを勧められたそう)
組み込みとかやりたいなら、まぁいっかとは思いつつも
大学で授業受けたりするわけでもなく独学でのファースト言語がJavaだとつまずきそうだなぁ~と思ってた
(別にJavaをディスってるいるわけではないのでご了承ください)

1ヶ月ぐらいは適度に連絡を取り、状況報告を受けていた
基礎の文法が終わって今からポートフォリオ(自作のRPGだそう)を作り出すというところですという連絡が最終連絡だった
それから音沙汰なし。こちらから連絡するも返事は無かった

しかし、2ヶ月ぐらい経った頃、再び彼と会う機会があったので現状の確認がてら話を聞いてみた(Aとしとく)

僕「最近の学習の調子はどうなのよ?」
A「いや、正直飽きてしまいました」
(やっぱりな〜)
僕「何が面白くなかった?」
A「あっちをいじるとこっちがダメになって訳の分からないエラーが出ての繰り返しで...」
A「あと完成見えなくてモチベーションが保てません」

だいたい以下のどれかにつらみを感じて継続不可になる
いい感じに初学者あるあるにハマってるなと

  • 入門言語の難易度の問題(学習コスト)
  • 学び方の問題
  • モチベーションの保ち方(進め方)の問題

Aの場合は
=> 入門言語の難易度の問題 から モチベーションの保ち方(進め方)の問題 へのリレーだった

そんなこんな話を聞いて一度、Pythonを教えてみることになった
以下、Pythonの入門記事ではないのでさらっと流してください

何を教えたのか

プログラミング言語のレクチャーで何に触れたのかをざっくりとまとめる
完全な初心者ではないものの、一度0からやりたいとのことだった

hello world
最も基本的な値の表示

print("hello world!")

値の型について
値には種類があるんやで〜という話

num = 3 #integer
text_val = "hello world" #string
f_num = 3.14 #float
is_match = True #bool

演算子
基本的なものだけ

5+6 #11
3.14 * 2 #6.28
"hello" + " world" #hello world
5 > 4 #True
5 < 4 #False
"hello" == "hello" #True
"hello" != "hello" #False

ifの文法

num = 10
if num > 5:
  print("over")
else:
  print("lower")

num = 5
if num > 5:
  print("over")
elif num == 5:
  print("equal")
else:
  print("lower")

この時点で一度、FizzBuzzを解かせてみる
ifの判定順(3かつ5の倍数=15の倍数か)に悩むものの、割とすんなり解いてくれた
%演算については事前に教えた

num = 15

if num % 15 == 0:
  print("FizzBuzz") #FizzBuzz
elif num % 3 == 0:
  print("Fizz")
elif num % 5 == 0:
  print("Buzz")
else:
  print(num)

配列について
まず、こういうのやめようなって話をした
なぜ配列が必要なのかを伝えたかった

score1 = 98
score2 = 40
score3 = 89
score4 = 100
score5 = 32
:
:
score100 = 54

配列使おうぜ

nums = [1,2,3,4,5]
fruits = ["apple", "banana", "peach", "orange"]
any = [True, None, 1, "banana", 3.14]

#値の取得(indexは0から始まる)
nums[0] #1
nums[4] #5
nums[5] #error!

思った通り、配列から値を全部取り出すにはどうすればええですか?と質問が来る
forってのがあると自然な流れでループ処理に行けた

forの文法について
ざっくりと

nums = [1,2,3,4,5]

for num in nums:
  print(num)

# 1
# 2
# 3
# 4
# 5

for i in range(0, 4):
  print(i)

# 0
# 1
# 2
# 3

for i in range(4):
  print(i)


# 0
# 1
# 2
# 3

さっきのFizzBuzzをfor組み合わせて書かせたくなったので
range(101)でFizzBuzzをやってもらった
また後で詳細は記述するがこの時点で割とつまっていた
20分ぐらいして上手く動く

for num in range(101):
  if num % 15 == 0:
    print("FizzBuzz") #FizzBuzz
  elif num % 3 == 0:
    print("Fizz")
  elif num % 5 == 0:
    print("Buzz")
  else:
    print(num)

# FizzBuzz
# 1
# 2
# Fizz
# 4
# Buzz
# Fizz

関数について&文法
時間も差し迫っていたので最後に関数について触れた
まずは関数が何なのかという説明をする(厳密な定義ではない)

#関数って何? => 値をもらったらその値を変化/変換するやつ

def add_10(x):
  return x + 10
  
#もらった値に+10変化(変換)している
print(add_10(10))

def greet(user):
  res = "hello " + user
  return res
  
#user名にhelloを付与
print(greet("okb"))

総まとめ

最後にFizzBuzzのfor内の処理を関数化してもらった
この部分でもかなりつまった

def fizzbuzz_func(num):
  if num % 15 == 0:
    return "FizzBuzz"
  elif num % 3 == 0:
    return "Fizz"
  elif num % 5 == 0:
    return "Buzz"
  else:
    return num

for num in range(0, 101):
  print(fizzbuzz_func(num))

#結果省略

1日で行なったことをまとめると

  • 値の出力
  • 変数と型について
  • 演算子
  • ifの文法
  • FizzBuzz
  • 配列
  • forの文法
  • 関数についてと文法
  • FizzBuzzの関数化
  • 他、いくつか例題

と割と盛りだくさんだった
この学習を通して何度かプログラミングを教える難しさを感じたので
前振りが長くなったが共有をしていく

print関数とかlen関数ってどこから呼ばれてるの? どこに書かれているの?

自分で関数を実装するようになって何度か聞かれた
どこにもprint()やらlen()を記述していないのになぜこれらのメゾットが使えるのかが分からないと言われた

def add_10(x):
  return x + 10

#どこにある?
#def print(val):
#:
#:
#:

print(add_10(5)) #15
#このprintはどこから呼ばれてるの!?

自分の中では「そういうもの、用意されているもの」という認識だったが
これが腑に落ちないらしい

この言語にはこういうメゾットがあって...という説明をすることで分かってもらえた
車を運転するのに内部構造を100%理解している必要はないのと同じことだが
低レイヤーの知識も説明できる程度に必要だと反省

書いた関数が実行されない

関数を遊びで作っている時に言われた

def greet(name):
  print("hello " + name)
  return True

「今、実装した関数が動かないんですが...」と言われコードを見てみると
関数の呼び出しが見つからなかった
これが書いてるだけで

def greet(name):
  print("hello " + name)
  return True
greet("okb")

という記述がない
これは関数は呼び出して初めて実行されるという認識が無かったために起きた

関数に取り組むまで単純なコードしか書いておらず
コードは上から下に順番に実行されるものだと(半分正解)思わせてしまった
関数は呼び出して初めて実行されるということをきちんと説明するべきだった

配列や関数の使い所がイメージできない

配列やら関数の文法を一通り、教えた後に例題に取り組んでいる時に発生した
書き方を知ったところで使い方が全くイメージできないとのこと

「配列や関数が便利なのは分かるが、じゃあ使うべきっていう境界線はどこにあるの?」

確かに。明確に言語化しようとするとかなり難しい
そもそも僕はプログラミングの記述に推奨はあっても100&の正解はないと思っている
同じ命題に対しても記述されたコードは余程のことがない限りは一致しない

完全パターンなんてものは存在しないため、「やってる内に分かってくるよ」としか言えなかった
自分の頭の中には「〜この時に使うべき」というべきレシピパターンがあるが言語化は出来ない

過去に自分が書いたコードを見せて、こういうところで使ったでと実物を見せてあげねばと反省
しかし、複雑すぎるものや可読性の悪いものを用意すると混乱を招くので要注意

総評

まぁ分かるけどピンとこない

今思えば、車を作るために各パーツの勉強をするのと
車を作ることを知らずに各パーツの勉強をするのとでは圧倒的に気持ちに差がでるなと思った

プログラミング教育では文法やお作法について最初に取り組むことが多いが
その前に、これからやることをマスターすることで最終的にこういったことが出来るようになります
ということを提示してあげられればと思った

理想形は先に作成物が決まっていて、文法を覚えながら作成物完成までのステップを辿っていくような
ディアゴスティーニのような形

これも個人によって好き嫌いが分かれるので用意するのは難しい

個人的所感をまとめておくと

  • 文法のゴリ押しはNG
  • 知ってる前提で話さない

またAにPythonを教える機会がありそうなので適宜、レポートをまとめたいと思う

【実装コード有り】Elixirで近しいデータを予想するためにk近傍法を実装した

k近傍法とは

ざっと説明するとAというデータとBというデータが
どれだけ近しいかを予測するためのアルゴリズム
仕組みは超簡単で以下のようなデータがあったとする

size(大きさ)は1~5の5段階
is_red(見た目)は1~5の5段階
has_seed(種があるかないか)は0or1

apple = %{size: 2, is_red: 5, has_seed: 0}
water_melon = %{size: 5, is_red: 1, has_seed: 1}

まずこのappleとwater_melonの2つのデータの距離(どれだけ似ているか)を計算する
値が小さければ小さいほど距離が近いということになる
算出にはシンプルに三角比でお馴染みのピタゴラスの定理を使う
ユークリッド距離と呼ばれているらしい

Elixirで計算してみると以下のようになる

  • :math.pow() : 乗数を算出するErlangメゾット
  • :math.sqrt() : 平方根を算出するErlangメゾット
sum_val = :math.pow(2-5, 2) + :math.pow(5-1, 2) + :math.pow(0-1, 2)
dis = :math.sqrt(sum_val)

iex> dis
5.0990195135927845

試しに青リンゴのデータを用意して距離を算出してみる

blue_apple = %{size: 3, is_red: 1, has_seed: 0}
sum_val = :math.pow(2-3, 2) + :math.pow(5-1, 2) + :math.pow(0-0, 2)
dis = :math.sqrt(sum_val)

iex> dis
4.123105625617661

さきほどよりは小さくなったが色の特徴量の数値が値に大きく影響しているのが分かる
(特徴量選択や正規化には触れない)

appleとの距離の算出結果が複数個あるとする(label: 分類名, dis: 距離)

cal_res = [
  %{label: :apple, dis: 1.0},
  %{label: :water_melon, dis: 5.0},
  %{label: :water_melon, dis: 2.0},
  %{label: :apple, dis: 2.5},
  %{label: :apple, dis: 1.5},
]

k近傍法では距離の値の小さいものからk個取得して多数決をとる
k = 3としてデータを取得する

fetch_data = [
  %{label: :apple, dis: 1.0},
  %{label: :water_melon, dis: 2.0},
  %{label: :apple, dis: 1.5},
]

取得したデータのラベルをまとめると

[:apple, :water_melon, :apple]

となり、このデータで多数決を取ると最終的な予想結果はappleとなる

Elixirでの実装

与えるデータがappleとorangeのどちらに似ているかを予想させてみる

fruits = [
  %{size: 2, is_red: 2, label: :orange},
  %{size: 2, is_red: 3, label: :orange},
  %{size: 4, is_red: 4, label: :apple},
  %{size: 1, is_red: 1, label: :orange},
  %{size: 5, is_red: 5, label: :apple},
  %{size: 3, is_red: 4, label: :apple},
]

#このデータはapple? orange?
target_data = %{size: 4, is_red: 3}

#取得する値の数(k)
knn_neighbor_num = 3

まずは全体のコードと結果を表示

defmodule KNN do
  def main(target_data, datas, knn_neighbor_num) do
    calc_res = calc_distance(target_data, datas, knn_neighbor_num)
    count_res = count_common_val_in_lst(calc_res)
    Enum.reduce(Map.keys(count_res), fn x, accum -> 
      if count_res[x] > count_res[accum] do
        x
      else
        accum
      end
    end)
  end
  def calc_distance(target_data, datas, knn_neighbor_num) do
    datas
      |> Enum.map(fn row ->
          calc_res = Enum.map(Map.keys(target_data), fn z -> :math.pow((target_data[z] - row[z]),2) end)
          %{score: :math.sqrt(Enum.sum(calc_res)), label: row[:label]}
        end)
      |> Enum.sort_by(&(&1.score))
      |> Enum.slice(0..knn_neighbor_num-1)
      |> Enum.map(&(&1.label))
  end

  def count_common_val_in_lst(lst), do: _count_common_val_in_lst(lst, %{}, [])
  defp _count_common_val_in_lst([], accum, _), do: accum
  defp _count_common_val_in_lst([head | tail], accum, labels) do
    case Enum.member?(labels, head) do
      true ->
        n_accum = Map.put(accum, head, accum[head]+1)
        _count_common_val_in_lst(tail, n_accum, labels)
      false -> 
        n_accum = Map.put(accum, head, 1)
        _count_common_val_in_lst(tail, n_accum, labels ++ [head])
    end
  end
end

IO.puts(KNN.main(target_data, fruits, knn_neighbor_num))
#apple

与えたデータはk近傍法ではappleと予想された
確かに、fruitsのデータと比べてみるとappleっぽい(適当

各関数について

calc_distance

ターゲットデータとその他(今回でいうとfruits)の距離の算出を行なっている
特徴量の数は変動するのでターゲットデータからkeyを生成するようにした
パイプとEnumめっさ使ってElixirっぽく書けた気がする

def calc_distance(target_data, datas, knn_neighbor_num) do
  datas
     |> Enum.map(fn row ->
        #列挙したデータとターゲットデータの距離を算出
        calc_res = Enum.map(Map.keys(target_data), fn z -> :math.pow((target_data[z] - row[z]),2) end)
        %{score: :math.sqrt(Enum.sum(calc_res)), label: row[:label]} 
      end)
    |> Enum.sort_by(&(&1.score)) #scoreの順にリストinマップをソート
    |> Enum.slice(0..knn_neighbor_num-1) #上位k個だけを取得
    |> Enum.map(&(&1.label)) #keyがラベルの値(appleとかorange)を返す
end

count_common_val_in_lst

calc_distanceの戻り値を使って各ラベルがいくつあるのかをカウントする
[apple, apple, banana, orange, orange, apple]というデータからは

res = %{
  apple: 3,
  banana: 1,
  orange: 2
}

というようなマップを生成する

def count_common_val_in_lst(lst), do: _count_common_val_in_lst(lst, %{}, [])
defp _count_common_val_in_lst([], accum, _), do: accum
defp _count_common_val_in_lst([head | tail], accum, labels) do
  #labels(記入したラベルを保持するためのaccum)にheadが存在しているかどうか
  case Enum.member?(labels, head) do
    true ->
      n_accum = Map.put(accum, head, accum[head]+1) #指定のラベルがkeyの値を上書き
      _count_common_val_in_lst(tail, n_accum, labels)
    false -> 
      n_accum = Map.put(accum, head, 1) #新規のラベルをkeyとして作成して1をセット
      _count_common_val_in_lst(tail, n_accum, labels ++ [head])
  end
end

main

上の2つの関数を組み合わせてcountしたmapを作成して
その値から一番カウントが大きな値を返す

def main(target_data, datas, knn_neighbor_num) do
  calc_res = calc_distance(target_data, datas, knn_neighbor_num)
  count_res = count_common_val_in_lst(calc_res)
  Enum.reduce(Map.keys(count_res), fn x, accum -> 
    if count_res[x] > count_res[accum] do
      x
    else
      accum
    end
  end)
end

データはこんな感じで変化していく

#calc_distance
calc_res =[
  %{score: 1, labels: apple},
  %{score: 4, labels: orange},
  %{score: 2, labels: apple},
]

#count_common_val_in_lst
count_res = %{
  apple: 2
  orange: 1
}

#last return
--> apple

可能な限りEnum.reduceとかでやりたい反面、中々上手くいかずに再帰関数を使うハメになる
個人的にaccumを2つもてるEnum.reduce_neo()みたいなのを作ろうと思った

【実装コード有り】Elixirでタイポ発見器を最長共通部分列を使って作る

プログラムでタイプミスを修正する難しさ

以下のような変数があったとする
user_input_textは自身で入力したテキストで"apple"と入力したつもりが
間違えて"anpple"と入力してしまっている

user_input_text = "anpple"
answer_text = "apple"

他にもタイプミスはいろんなパターンが考えられる

"aple"
"aale"
"applue"
"apule"

少し大袈裟なものもあるが、人間視点でいえば
「あ、こいつはappleって打ちたかったんやな〜」ってことが何となく分かる
従って添削する場合にappleと修正してあげれば良い

しかし、機械視点ではタイプミスから共通のパターンを見つけて修正するのは難しい

"aple" => "apple"
"aale" => "apple"
"applue" => "apple"
"apule" => "apple"

こんな時に使えるのが最長共通部分列たるもの

最長共通部分列とは

説明するよりも結果見た方が早いので"apple"と"anpple"の最長共通部分列を求めてみる
まずはこんな感じのテーブルを用意

wrong/answer a n p p l e
a - - - - - -
p - - - - - -
p - - - - - -
l - - - - - -
e - - - - - -

まずはのanswerの"apple"の"a"から見ていく
"a"の視点で行に"a"があった場合に、"a"の行に1で埋め込む

wrong/answer a n p p l e
a 1 #発見 1 1 1 1 1
p - - - - - -
p - - - - - -
l - - - - - -
e - - - - - -

さらにwrongのa列を調査済みにするために1で埋め込む

wrong/answer a n p p l e
a 1 1 1 1 1 1
p 1 - - - - -
p 1 - - - - -
l 1 - - - - -
e 1 - - - - -

この作業を繰り返し行う
値が一致しない場合は現在の最大値(この場合では1)を埋め込む

wrong/answer a n p p l e
a 1 1 1 1 1 1
p 1 1 2 2 2 2
p 1 1 2 - - -
l 1 1 2 - - -
e 1 1 2 - - -
wrong/answer a n p p l e
a 1 1 1 1 1 1
p 1 1 2 2 2 2
p 1 1 2 3 3 3
l 1 1 2 3 - -
e 1 1 2 3 - -
wrong/answer a n p p l e
a 1 1 1 1 1 1
p 1 1 2 2 2 2
p 1 1 2 3 3 3
l 1 1 2 3 4 4
e 1 1 2 3 4 -
wrong/answer a n p p l e
a 1 1 1 1 1 1
p 1 1 2 2 2 2
p 1 1 2 3 3 3
l 1 1 2 3 4 4
e 1 1 2 3 4 5

これでテーブルの埋め込みは完了 テーブルの最大値が最長共通部分列となる
今回は最長共通部分列が5となった

正解テキストが"banana"の場合はどうなるか
埋め込み過程については同作業なので省略

text a n p p l e
b 0 0 0 0 0 0
a 0 0 0 0 0 0
n 0 0 0 0 0 0
a 0 0 0 0 0 0
n 0 0 0 0 0 0
a 0 0 0 0 0 0

こんな感じの算出をプログラムで行いたい

Elixirでの実装

先にコードと結果をお見せします
Enum.reduce使ってスマートに書けないなぁと色々と試行錯誤したものの断念
アキュムレーターを2つ持たせられたり、条件に一致したら列挙を中止するなどがEnumでできたら嬉しい

defmodule Sample do
  def main(input_val, ans_val) when input_val == ans_val, do: :good
  def main(input_val, ans_val), do: _main(String.graphemes(input_val), ans_val, 0)
  defp _main([], _, counter), do: counter
  defp _main([head | tail], ans_val, counter) do
    str_lst = String.graphemes(ans_val)
    [_n_head | n_tail] = str_lst
    case type_judge(str_lst, head) do
      :hit -> _main(tail, List.to_string(n_tail), counter+1)
      :empty -> _main(tail, List.to_string(n_tail), counter)
    end
  end
  
  def type_judge([], _), do: :empty
  def type_judge([head | tail], compare_str) do
    if String.contains?(compare_str, head) do
      :hit
    else
      type_judge(tail, compare_str)
    end
  end
end

簡単なテストの実行をしてみる

tests = [
  Checker.main("apple", "anpple") == 5,
  Checker.main("apple", "apple") == :good,
  Checker.main("banana", "anpple") == 0,
  Checker.main("banana", "anapple") == 1,
  Checker.main("fish", "fosh") == 3
]

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

良い感じですね
動作に関しては問題なさげだけど、もっとシンプルなコードにしたい

コードの解説

main関数

#入力と答えの候補が一致している場合は終了
def main(input_val, ans_val) when input_val == ans_val, do: :good

#文字列をリストに変換して_mainに渡す
def main(input_val, ans_val), do: _main(String.graphemes(input_val), ans_val, 0)
defp _main([], _, counter), do: counter
defp _main([head | tail], ans_val, counter) do
  #正解テキストをリストに変換
  str_lst = String.graphemes(ans_val)
  #次の再帰用に先頭文字列を削除(スライスでやれればいいが...)
  [_n_head | n_tail] = str_lst
  case type_judge(str_lst, head) do
    :hit -> _main(tail, List.to_string(n_tail), counter+1)
    :empty -> _main(tail, List.to_string(n_tail), counter)
  end
end

type_judge
コードに関しては説明することは特になし
貰った文字列同士が一致しているか調べてるのみ
一致した場合に再帰せずに終了

def type_judge([], _), do: :empty
def type_judge([head | tail], compare_str) do
  if String.contains?(compare_str, head) do
    :hit
  else
    type_judge(tail, compare_str)
  end
end

今思えばこれただの「==」でよかったなと
思った通り、普通に動いてる

def type_judge([], _), do: :empty
def type_judge([head | tail], compare_str) do
  if compare_str == head do
    :hit
  else
    type_judge(tail, compare_str)
  end
end

tests = [
  Checker.main("apple", "anpple") == 5,
  Checker.main("apple", "apple") == :good,
  Checker.main("banana", "anpple") == 0,
  Checker.main("banana", "anapple") == 1,
  Checker.main("fish", "fosh") == 3
]

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