やわらかテック

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

【レポート】第5回清流elixir勉強会in丸の内を開催しました【再帰関数】

トピック

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

先週にはfukuoka.exを運営されているpiacereさんと Twitterで繋がらせて頂きまして多くの方に清流elixirの名を知って頂けました
本当にあざます!!!
やる気がめっさ出ました!!

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

第5回の活動内容

関数型言語といったらやっぱり再帰関数でしょってことで軽いノリでこのテーマに
【自分的レシピ】elixirでの再帰関数の動かし方でも触れたように
関数型言語には一般的にfor文のような、いわゆるループ処理は用意されていない
じゃあ、どうやって書くのよ?という問いに対する答えは「再帰関数」を使おう

再帰関数ってなんぞ

関数が自分自身を呼び出す処理のこと
5回だけ「"hello"」と出力する再帰関数を作るとする
コードに落とし込むとこんな感じ

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

Sample.hello()

関数が自分自身を呼び出しているのが分かる
ただし、このままこのコードを実行するとまずい。無限再帰になってしまう
「いつ停止するの?」という問題がある
今回は5回「"hello"」を出力させた時点で再帰を終了させる必要がある
これは「停止性」といい、本当にこのコードが終了するかを保証する必要がある(停止性の議論)

たとえば5回で終了させたいならばこんな風に書き換える

defmodule Sample do
  def hello(counter) do
    if counter == 5 do
      :fin
    else
      IO.puts("hello")
      hello(counter+1)
    end
  end
end

Sample.hello(0)
# hello
# hello
# hello
# hello
# hello

きちんと再帰が停止した
helloの引数に追加したcounterというのはアキュムレーターと呼ばれるもので
貯蔵庫のような意味があり、状態を保管するために使用している
今回の場合は何回呼び出したか?ということをカウントするためのカウンターとして扱っている
アキュムレーターについての詳しく話はこちらで解説しているのでぜひ

ただこのコードはElixirっぽいくない上にダサいので書き換える

defmodule Sample do
  def hello(counter) when counter == 5, do: :fin
  def hello(counter) do
    IO.puts("hello")
    hello(counter+1)
  end
end

Sample.hello(0)
# hello
# hello
# hello
# hello
# hello

これでも上手く動く

defmodule Sample do
  def hello(5), do: :fin
  def hello(counter) do
    IO.puts("hello")
    hello(counter+1)
  end
end

参加者の方が面白い書き方を発見した(凄い
「helloにdefaultの値を持たせたらhelloの呼び出し時に0を渡す必要がなくなるのでは?」 fmfm...

defmodule Sample do
  def hello(5), do: :fin
  def hello(counter \\ 0) do
    IO.puts("hello")
    hello(counter+1)
  end
end

Sample.hello()

実行すると上手く動くが以下のような警告が出る

warning: definitions with multiple clauses and default values require a header. Instead of:
def foo(:first_clause, b \ :default) do ... end
def foo(:second_clause, b) do ... end

errorの意味は分かるが、修正の方法が思いつかず
再帰関数作るときはアキュムレーターにdefault引数渡すの良くなさそうということで一旦落ち着いた
詳しい原因をご存知の方は教えてください

再帰関数で色々作って遊んでみる

f:id:takamizawa46:20190525130609j:plain:w450

配列の先頭の要素を取得して出力する
headとtailを使って実装する
リストが空となった場合に停止するように関数を実装する

sample = [1,2,3,4,5]
[head | tail] = sample
defmodule Sample do
  def fetch([]), do: :fin
  def fetch([head | tail]) do
    IO.puts(head)
    fetch(tail)
  end
end

Sample.fetch([1,2,3,4,5])
# 1
# 2
# 3
# 4
# 5

配列の要素を合計する
アキュムレーターに先頭の要素をどんどん足していく

defmodule Sample do
  def sum([], accum), do: accum
  def sum([head | tail], accum), do: sum(tail, accum+head)
end

IO.puts(Sample.sum([1,2,3,4,5], 0))
#15

このままだと呼び出し時に0を渡すという操作があり
アキュムレーターの知識がない人には煩わしいのでhelper関数というものでラップする

defmodule Sample do
  def sum(lst), do: _sum(lst, 0)
  defp _sum([], accum), do: accum
  defp _sum([head | tail], accum), do: _sum(tail, accum+head)
end

IO.puts(Sample.sum([1,2,3,4,5]))
#15

これならリストを渡すだけでsumをアキュムレーターを意識せずに使用することが可能

配列の大きさをカウントする

defmodule Sample do
  def length([], accum), do: accum
  def length([_head | tail], accum), do: length(tail, accum+1)
end

IO.puts(Sample.length([1,2,3,4,5], 0))
#5

良い感じですね
言語に実装されているEnumの関数は再帰関数を使えば再現することが可能
明日から新たなモジュールを開発できる。やったぜ

悲しいお知らせ

本日の勉強会で作成した再帰関数はお察しの通り、大体はEnumの関数やらの組み合わせで作成することが可能
再帰関数を作成するまえに必ず、この処理はEnumやらを使って実装することが出来ないかを考える必要がある
再帰関数で書いてもいいけどね。スピード感を大事にしたい

嬉しいお知らせ

20190601(土)に東京で開催される
Erlang & Elixir Fest 2019に行って参ります
恥ずかしながら、東京にいくのは人生で2度目で迷わずに行けるかが心配..

清流elixirを代表して参加しようと思っていたら
いつも勉強会に来て頂ける参加者の方が全員参加するようでワロタ

こんなすげー方達の話を聞ける機会は滅多にないので楽しんできます
次回の勉強会はElixirの実装力を上げるために競プロ問題をいくつかElixirで解いてみようと思ってます

【続編】Responderで作成したAPIをheorkuにデプロイするまで

やりたいこと

www.okb-shelf.work

前回の記事でResponderを使って作成したAPIをherokuにアップしたい
野望としてはDockerに乗せてアップしたかったのだが、時間的な都合もあり
とりあえずはDocker無しで動くものを一旦デプロイした

公式のサンプルが当てにならなかったのでまとめておく

herokuへのデプロイ

以前、作成したコードを一部抜粋

import responder

api = responder.API()

@api.route("/")
def first_api(req, resp):
  resp.text = "first success!!"

if __name__ == '__main__':
  api.run(address="0.0.0.0")

localhostを呼び出すと「"first success!!"」が返ってくるのみ

まずはheroku にappを作成する

> heroku create

git経由でherokuにupしたいので

> git init
> git remote add heroku (herokuのappのpath)

をセット

次にheroku用にファイルを変更 & ファイルを追加

#api.runのaddressに0.0.0.0を追記
if __name__ == '__main__':
  api.run(address="0.0.0.0")

Procfileを作成して以下を追加

web: python server.py

このままpushするとこんなようなエラーが出ることがある

error: failed to push some refs to .....

こいつはPipfile.lockが原因で発生していたようだ
Pipfile.lockを先に削除しておく

あとはいつも通り、herokuにpushする

> git add .
> git commit -m "first commit"
> git push heroku master

pipenvを使っているのでインストールに少し時間がかかる
無事にデプロイされたかを確認してみる

> curl https://pacific-temple-90936.herokuapp.com/
first success!!%

あ、いいですね
デプロイに関しては以上です。以下自己満足

docker-composeでのResponder環境の作成

公式ドキュメントに記述があるが
僕の手元ではこのDockerfileは上手くrunできなかった(COPYが上手くいってないっぽい)

他のDocker構成を探してこちらの方のものを参考にした
pythonをベースにpipenvをinstall

FROM python:3.6.4
RUN mkdir /api
WORKDIR /api
ADD Pipfile /api/
RUN pip install --upgrade pip \
    && pip install pipenv \
    && pipenv install
ADD . /api/

次にdocker-compose.ymlを作成
pipenvを使ってpythonファイルを実行する

responder:
  build: .
  command: sh -c "pipenv install && pipenv run python server.py"
  ports:
    - "5042:5042"
  volumes:
    - .:/api

buildしてupして動作を確認してみる

> docker-compose up -build

無事にbuildが完了してサーバーが立ち上がる

Building responder
Step 1/6 : FROM python:3.6.4
 ---> 07d72c0beb99
Step 2/6 : RUN mkdir /api
 ---> Using cache
 ---> 7b7862fffcbf
:
:
Successfully built __________
:
:
responder_1  | INFO: Uvicorn running on http://0.0.0.0:5042 (Press CTRL+C to quit)

お馴染みに試す

> curl http://0.0.0.0:5042/
first success!!

環境を用意することは出来たが...

悩みの種

こいつをherokuにデプロイするところで詰まっている
今回は仕事の関係で時間制限があったため、止む無く上記の方法を選択した
herokuの方ではH=10やらメモリーオーバーのerrorになる
原因が分からず切磋琢磨中....また進展ありましたらブログを更新します

VSCode使って初学者とコードレビュー兼ペアプロしてみた感想

やってもらった課題

未経験者にプログラミングを教えて得られた知見と反省点と登場したA君に
やってもらっていた課題が完成しましたとの報告が届いた

ちなみにどんな課題をやってもらっていたかというと

name score subject
Kennith Kling 18 math
Oliver O'Connell 36 math
Nicole Gutkowski 6 math
Blanche Deckow 19 math

こんな感じの.csvファイルがあって
このファイルをpythonで読み込んでscore(点数)を合計してprintするというもの
pandasやらは使わずにwith open使って集計してほしいなと淡い期待を抱いて課題を作った

sum_ = 18 + 36 + 6 + 19
print(sum_) 

変数から関数の実装程度までは以前、触ってもらっていたのであまりハマるポイントはないかなと思う
かろうじて、ハマるかなと思っていたのはcsvのheaderを読み飛ばす部分
まぁググれば出るし大丈夫でしょ(適当
あとは合計用の変数を上手く扱ってくれるかどうか(scope問題->毎回0になってる)

完成したとの報告が

課題を出して2日ぐらいで「とりあえず完成しました」との報告があった
ただ平日で面会できる時間も取れないのでどうしたものかと思ったが
そういえばVSCodeペアプロが出来るようになったことを思い出し、急いで調べてペアプロ環境を用意した

VSCode拡張機能の検索欄で「Live Share」と検索してインストール
そうするとVSCodeの下部にLive Shareという項目が追加されるのでこいつをクリック
f:id:takamizawa46:20190519163514p:plain:w450

クリックするとリンクがコピーされるのでペアプロをしたい相手にこのリンクを教えてあげる
あとはこのリンクをクリックしてもらうだけで相手がやってくる
f:id:takamizawa46:20190519170347p:plain:w550
嬉しさのあまりSkype繋いでいるのに画面上でチャットを始める

まずは彼が作成してくれたコードを原文のままにご紹介する

import csv


read_file = "score.csv"

with open(read_file, newline='') as csvfile:
    reader = csv.reader(csvfile, delimiter=',', quotechar='|')

    header = next(reader)
    scores = 0
    for data in reader:
        scores = scores + int(data[1])

うん、とりあえず懸念していた点は全てクリアしてきてくれた!!
これはアカンなって気になる点は特にはないがこのままだと使い勝手が少々悪いので
まずは関数化してもらうことにした

関数化する理由を納得させることが出来ない

しかし、どうやらまだ関数化という考え方に若干ピンと来ていないようで
どういう時に関数化にするんですか?とよく聞かれる
完全な関数化条件は無いものの、「使い回したい時、その可能性がある時」と答えている

ペルソナを考えてみると分かりやすいのかな
彼が作成したこのコードを数学のテスト結果がまとめられた.csvファイルにのみ実行させていたが
他教科の採点もしたくなったとすると
このままだとコードを直接編集して書き直す必要がある

#read_file = "score.csv"  
read_file = "score2.csv"

これは面倒なので少なくともargment parserは使わないとして
関数に引数渡しで対象のファイルを変更できるようにはしておきたいという発想になる

これは面倒なコード編集をした過去があるからそう思うのかもしれない
それを伝えたいのだが中々、言葉足らずになってしまう

まぁとりえあず文法覚える意味も込めてやってもらうことにした

関数を作成する際のレシピ

いつもどういう事を思って関数を作ってるのかということを聞かれたので
自分なりのレシピを書き出して説明してみた(受け売りですが

## 関数を作る時のレシピ
### 何がしたいのか
### 何の値を受け取るのか(引数は何)
### 何の値を返すのか
### 関数の名前(意外と重要)

これを上から埋めてもらった。今回作りたいファイル名を受け取って合計点を算出するというプログラムではこんな感じに落ち着いた

## 関数を作る時のレシピ
### 何がしたいのか,何ができるのか 
--> csvファイルを開いて点数を合計する
### 何の値を受け取るのか(引数は何) 
--> テスト結果ファイル
### 何の値を返すのか
 --> 合計した点数
### 関数の名前(意外と重要)
 --> goukei

このレシピを元に関数を作ってもらった
文法でつまずく(indentやらreturn)部分はあったが本人も納得して関数を作ってくれた
VSCodeで同時編集しながら彼がどういう過程でコードを書いていくかという事を見ることが出来るため
自分の盲点になっている点が多く説明が良く無かったなと非常に勉強になる
あと自分が本質的に理解出来ていなかった部分が浮き彫りになったりする(print関数はどこに実装されてるのか(前回参照))

逆に自分がどのような過程でコードを生成しているかを見せられるので速習になるのではないかと勝手に思っている

def goukei(file):
    with open(file, newline='') as csvfile:
        reader = csv.reader(csvfile, delimiter=',', quotechar='|')
        header = next(reader) #headerを読み飛ばす
        scores = 0
        for data in reader:
            scores = scores + int(data[1])
    return scores

そしていかにファイルの変更が楽に行えるかを説明する

read_file = "score2.csv"

with open(read_file, newline='') as csvfile:
    reader = csv.reader(csvfile, delimiter=',', quotechar='|')

    header = next(reader)
    scores = 0
    for data in reader:
        scores = scores + int(data[1])

print(scores)

#--------------------------

read_file = "score2.csv"
: #省略
:
print(scores)

#関数なら?
res1 = goukei("score2.csv")
res2 = goukei("score.csv")

その場でついでにもう1つ課題を与える

時間があったので関数化のレシピを使いつつ、もう1つ課題をこなしてもらった

こういうデータがある

data = [
    ["A", "B", "A", "AB", "O", "O"],
    ["B", "B", "AB", "A", "B", "O"],
    ["O", "B", "A", "B", "A", "O"],
    ["A", "A", "A", "AB", "O", "A"],
    ["A", "O", "A", "AB", "O", "O"],
    ["B", "B", "A", "O", "O", "O"],
]

このデータ(2次元のリスト)から指定の血液型がいくつ含まれているかをカウントしたい

print(bloody_count("A")) #n
print(bloody_count("O")) #m

説明する事も特になく、すんなりと実装してくれた
ついに配列に用意された.count関数を使い始めた。言語の関数を使って簡略化することは非常に良い事

def bloody_count (blood_type):
    all = 0
    for bl_count in data:
        all += bl_count.count(blood_type)
    return all

print(bloody_count("A")) #12
print(bloody_count("AB")) #4
print(bloody_count("O")) #12
print(bloody_count("B")) #8

VSCodeで作成する過程を見ていると先ほどと手を動かす速さが違うことに気づく
何をすればいいかということが頭の中で理解できている状態であれば実装ってのは割とすんなり出来るんだなと実感
やっぱりペアプロっていいね。誰が教えてください

おまけのコーナー

なんでデータいじくる系の課題ばかりをやらせているの

これは完全にElixirの思想に染まっているからかもしれない
プログラミングでやることってのは大概がデータ処理
Aを変換してBにするという連鎖であって1つ1つは大したことはしていない

まずはデータをいじくる過程を体感してもらうことを大切にしている
個人的には、いきなりDjangoとかRailsとかをやらせるのには反対

データの動きで見えない部分が多すぎるからです
かといって文法だけをひたすら覚えさせるのも良くない
どういうデータ処理を行う場合に使えるのかを合わせて覚える必要がある
そのためにもこういう課題を用意している

教える際に気をつけている事

とりあえずは実装してもらうことを優先している
あきらかにその使い方はないでしょと思っても言わずに、その方法で解決できるように導くようにしている
その後で「実はこういうやり方があってね?」と自分の中でより楽に済む方法を紹介して「確かに」と思ってもらうようにしている
前が見えずにコードを書いている人に、それはダメといきなり言っても理解してもらえずモチベーションを奪うだけなので

【超簡単】PythonとResponderで爆速でAPIを立ち上げるまで

Responderについて

github.com

2018年の10月ぐらいに公開されたPythonのWebフレームワーク
当時からスター稼ぎまくりのおばけプロジェクトだった たまたま仕事で触る機会があったので自分のメモがてらまとめておく

Python界隈では有名な方が作成しており
FlaskとFalcon(これはやったことない)のいいとこ取りな上に
独自の設計思想が加えられて強靭無敵最強のフレームワークに出来上がっている

Responderの売りは恐ろしいぐらいのシンプルさ
Responderをimportするだけでもう使える

import responder

Elixirでのtrotでも同じようなことを言っていますが
今の流行りはシンプルさと制作がいかに早く行えるかということ
その点Responderは両方を兼ね備えている。うーん、良い
学習コストもかなり低く、1時間ぐらいドキュメントを見ただけで何と無く書けてしまう

responderのインストールとサーバーの立ち上げ

公式ではpipenvを使うことを推奨している
無くてもinstall & 使用は可能だけど、素直に従う
pipenvのinstallも簡単で以下を叩くのみ

> pip install pipenv

そしてpipenvを使ってresponderをinstallする
あと、ついでに後にjanomeを使うのでついでにinstall

> pipenv install responder --pre

#これはしなくてもok
> pipenv install janome

では早速、responderでAPIを作成する
その前にプロジェクト用のディレクトリを用意しておく

mkdir cool_api
cd cool_api

作成したファイルに以下を追加
./cool_api/server.py

import responder
api = responder.API()

if __name__ == '__main__':
  api.run()

pipenvを使って作成したserver.pyを実行する

> pipenv run python server.py
INFO: Started server process [62605]
INFO: Waiting for application startup.
INFO: Uvicorn running on http://127.0.0.1:5042 (Press CTRL+C to quit)

やったぜ。上手くserverが立ち上がった
デフォルトでは5042番ポートを使用してサーバーが立ち上がるが

api.run(port=8000)

とすることでポートを変更することが可能
ただ、この状態では一切のendpointが無いので無能of the無能なため
簡単なendpointを実装する

シンプルな実装例(GETとPOST)

まずはシンプルなテキストを返すGET(/)を実装する
./cool_api/server.py

@api.route("/") #endpointを設定
def first_api(req, resp): #引数は基本的に固定
  resp.text = "first success!!" #respに値を設定することで戻り値を作成

早速呼び出す

> curl http://localhost:5042/
first success!!%

上手く返ってきた
続いてjsonを返してみる
./cool_api/server.py

@api.route("/info")
def human_info(req, resp):
  #jsonを返したいときは.mediaを指定する
  resp.media = {"user": "okb"}
> curl http://localhost:5042/info
{"user": "okb"}%

いい感じっすわ

ページのレンダリングとbodyの値の取得

せっかくなので触れておく
まずはページのレンダリングから
server.pyの初回実行時に./cool_api の直下に
/static と /templates が作成されていると思う
/templates 直下に適当にindex.htmlを作成する
responderはデフォルトでjinja2をラップしているのでimport無しで使える
jinja2についての説明はカット

./cool_api/templates/index.html

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8">
    <title>Simple home</title>
  </head>
  <body>
    <h1>Welcome to my home</h1>
    <ul>
      <li>my name is okb</li>
      <li>I like elixir</li>
      <li>nice to meet you</li>
    </ul>
    {% block content %}{% endblock %}
  </body>
</html>

/main をcallした時にこのファイルを返すようにする
./cool_api/server.py

@api.route("/main")
def simple_homepage(req, resp):
  #.htmlファイルを返すときはresp.htmlを設定 & ファイル名を指定
  resp.html = api.template("index.html")

とりあえずcurlで呼び出す
./cool_api/templates/index.html

> curl http://localhost:5042/main
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8">
    <title>Simple home</title>
  </head>
  <body>
    <h1>Welcome to my home</h1>
    <ul>
      <li>my name is okb</li>
      <li>I like elixir</li>
      <li>nice to meet you</li>
    </ul>

  </body>
</html>%

ブラウザで見れば上手くレンダリングされてるはず

最後にPOSTで送られてきたbodyの値を受け取る
やり方さえ分かってしまえば簡単であとは今まで通り
Javascriptを触る方にはお馴染みだと思うasyncとawaitを使う
pythonでも使えるやってresponder使ってて知った
./cool_api/server.py

@api.route("/easy-post")
#asyncを指定する
async def easy_post(req, resp):
  data = await req.media() #ここで値を受け取る
  input_data = data.get("input", "None data..?")
  resp.text = f"fetch: {input_data}"

受け取った値をテキストで返す
では呼び出す

> url -H "Content-Type: application/json" -X POST -d '{"input": "apple"}' http://localhost:5042/easy-post
fetch: apple%

そこそこ便利なAPIを実装する

以前紹介&作成したjanomeを使用して日本語の形態素解析APIで行う
クラスメゾットに関しては使い回しですまぬ(若干アップグレードしてる
./cool_api/utils.py

import os
from janome.tokenizer import Tokenizer
import re

regex = r"(https?|ftp)(:\/\/[-_\.!~*\'()a-zA-Z0-9;\/?:\@&=\+\$,%#]+)"
pattern = re.compile(regex)

class JanomeSpliter():
  """return keywords(str) in array"""
  def __init__(self):
    self.t = Tokenizer()

  def parser(self, value_str, tag=u"名詞", validate=True):
    tokens = self.t.tokenize(str(value_str))
    if validate:
      res = [self.validate_str(token.surface) for token in tokens if token.part_of_speech.split(',')[0] == tag]
      res = list(filter(lambda x: x != None, res))
      return res
    else:
      res = [self.validate_str(token.surface) for token in tokens]
      res = filter(lambda x: x != None, res)
      return " ".join(res)

  def is_tag(self, value_str, tag):
    tokens = self.t.tokenize(str(value_str))
    res = [self.validate_str(token.surface) for token in tokens if token.part_of_speech.split(',')[0] == tag]
    return True if res else False

  def validate_str(self, value_str):
    if re.match('[あ-んア-ン一-鿐]+', value_str):
      return value_str

server.pyの上部に以下を追記
./cool_api/server.py

from utils import JanomeSpliter

j = JanomeSpliter()

APIを実装する。このAPIではレンダリングjsonの両方をサポートするようにする
URLの値を取得するには際にはendpointで実行する関数に引数を追加する
./cool_api/server.py

@api.route("/ja-parser/{mode}")
async def japanese_spliter(req, resp, *, mode):
  data = await req.media()
  user_input = data.get("input", False)
  if user_input:
    #受け取った値を形態素解析
    splited_input = j.parser(user_input, validate=False)
    if mode == "temp":
      #form.htmlに形態素解析結果を渡す
      resp.html = api.template("form.html", split_result=splited_input.split(" "))
    else:
      #jsonを返す
      resp.media = {"data": splited_input}

jsonを返すAPIを先にcallしておく

> curl -H "Content-Type: application/json" -X POST -d '{"input": "負けない思いを君に伝えたいよ"}' http://localhost:5042/ja-parser/api
{"data": "\u8ca0\u3051 \u306a\u3044 \u601d\u3044 \u3092 \u541b \u306b \u4f1d\u3048 \u305f\u3044 \u3088"}%

あ、何か文字化けしてますね
postman使って呼び出すときちんと形態素解析されている
postmanでの実行結果

{
    "data": "負け ない 思い を 君 に 伝え たい よ"
}

最後にレンダリング用に新規にform.htmlファイルを作成する
jinja2の構文についてはやっぱりカット

./cool_api/templates/form.html

{% extends "index.html" %}

{% block content %}
  <h2>parser form</h2>
  <!-- 送信先のendpointを指定(tempモード) --!>
  <form method="post" action="/ja-parser/temp">
    {{ form }}
    <!-- 受け取る際には"input"をkeyとして取得できる --!>
    <textarea name="input"></textarea>
    <button type="submit">execute</button>
  </form>
  {% if split_result %}
    <p>parse result</p>
    {% for token in split_result %}
      <p>{{ token }}</p>
    {% endfor %}
  {% endif %}
{% endblock %}

先ほどの/main でレンダリングするファイルをform.htmlに変更してブラウザから呼び出してみる
css当ててないのでデザインについてはお許しを...ッ!!

./cool_api/server.py

@api.route("/main")
def simple_homepage(req, resp):
  resp.html = api.template("form.html")

f:id:takamizawa46:20190519011954p:plain:w450

で、formに適当にテキストを入力してexecuteを押す
f:id:takamizawa46:20190519012124p:plain:w450

よし、上手く返ってきた。いいね

長々と書きましたがresponderの便利さが伝われば光栄です

【超簡単】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とかにデプロイする方法を調査してまた記事にでもします