やわらかテック

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

【Elixirで学ぶCS】Elixirで論理ゲートを実装するまで

なにこれ

僕は一応、理工学部の出身ではありますが建築土木が専攻でした
この業界にいながらcsについての知識が皆無
前からやらねば...やらねば..と思ってはいたが触れる機会がない & 一度挫折済み

しかし、アルゴリズムの勉強を始める中でデータ構造の重要性に気づき
データがコンピューター内部でどのように動いているか非常に気になった

良い機会だと思って「コンピュータシステムの理論と実装」を一読することに...
www.oreilly.co.jp

各説明は本を読んだ方が早いのでざっくりの説明で
実装部分をメインにブログで展開していくつもりです

ブール理論

コンピューターは一番下のレイヤーにいけば全て0と1(bool値)の組み合わせで構築されている
この組合せの数をたくさん増やす + 状態を保存(メモリやら)を使えばいろんな計算が出来るよねって話
このbool値を組み合わせて作るのが論理ゲートと呼ばれるもので

  • NOT
  • OR
  • AND

やらがある
さらにこれらの論理ゲートは全て、ベースとなる「NAND」という論理ゲートから作成することが可能
つまりはNANDさえ作れてしまえば全ての論理ゲートは実装可能となる
小さな世界から大きな世界が構築することが出来る(細胞が集まって生物になるみたいな感じ)

Elixirでの実装

NANDさえ作ってしまえば、他の論理ゲートは作成可能なのでまずはNANDから作成する

NANDゲート
NANDゲートは以下のような値を出力する関数とみなす

x y NAND(x, y)
0 0 1
1 0 1
0 1 1
1 1 0

ちょちょっと実装する
(1, 1)の時だけ0を返すようにしてある

defmodule RogicalGate do
  def nand(1, 1), do: 0
  def nand(_x, _y), do: 1
end

テストライブラリを使わずに簡単なテストを行なって
正しい値を出力しているかをチェックする

nand_test = [
  RogicalGate.nand(0,0) == 1,
  RogicalGate.nand(1,0) == 1,
  RogicalGate.nand(0,1) == 1,
  RogicalGate.nand(1,1) == 0,
]

Enum.map(nand_test, &(IO.puts(&1)))
true
true
true
true

あ、いいっすね

NOT
値を反転(0を1に, 1を0に)するのみ

x NOT(x)
0 1
1 0
defmodule RogicalGate do
  def nand(1, 1), do: 0
  def nand(_x, _y), do: 1
  def not_(x), do: nand(x, x) <-- 追加
end

テストを実行

not_test = [
  RogicalGate.not_(0) == 1,
  RogicalGate.not_(1) == 0,
]

Enum.map(not_test, &(IO.puts(&1)))
true
true

OR

x y OR(x, y)
0 0 0
1 0 1
0 1 1
1 1 1

NANDを3つ組み合わせて実装

defmodule RogicalGate do
  def nand(1, 1), do: 0
  def nand(_x, _y), do: 1
  def or_(x, y), do: nand(nand(x, x), nand(y, y)) <-- 追加
  def not_(x), do: nand(x, x)
end

テスト

or_test = [
  RogicalGate.or_(0, 0) == 0,
  RogicalGate.or_(1, 0) == 1,
  RogicalGate.or_(0, 1) == 1,
  RogicalGate.or_(1, 1) == 1,
]

Enum.map(or_test, &(IO.puts(&1)))
true
true
true
true

AND

x y AND(x, y)
0 0 0
1 0 0
0 1 0
1 1 1

すでに実装したnot_を使ってANDを実装する

defmodule RogicalGate do
  def nand(1, 1), do: 0
  def nand(_x, _y), do: 1
  def or_(x, y), do: nand(nand(x, x), nand(y, y))
  def not_(x), do: nand(x, x)
  def and_(x, y), do: not_(nand(x,y))
end

テスト

or_test = [
  RogicalGate.and_(0, 0) == 0,
  RogicalGate.and_(1, 0) == 0,
  RogicalGate.and_(0, 1) == 0,
  RogicalGate.and_(1, 1) == 1,
]

Enum.map(or_test, &(IO.puts(&1)))
true
true
true
true

いいっすね
次は作成したAND, NOT, ORを組み合わせてXORを作成してみる

XOR
(1, 0)か(0, 1)の際に1を出力させる
ORと異なるのは(1, 1 )の場合に0が返ってくるという点

x y XOR(x, y)
0 0 0
1 0 1
0 1 1
1 1 0

先ほど作成した論理ゲートをフルに使って実装する

defmodule RogicalGate do
  def nand(1, 1), do: 0
  def nand(_x, _y), do: 1
  def or_(x, y), do: nand(nand(x, x), nand(y, y))
  def not_(x), do: nand(x, x)
  def and_(x, y), do: not_(nand(x,y))
  def xor_(x, y), do: or_(and_(x,not_(y)), and_(not_(x),y))
end
xor_test = [
  RogicalGate.xor_(0, 0) == 0,
  RogicalGate.xor_(1, 0) == 1,
  RogicalGate.xor_(0, 1) == 1,
  RogicalGate.xor_(1, 1) == 0,
]

Enum.map(xor_test, &(IO.puts(&1)))
true
true
true
true

nビット論理ゲートへの対応

今まで作成したゲートをnビットの場合にも実行できるようにする
nビットといってもただ入力がリストになって複数入力されるだけで大したことはない

こんな関数を用意した
入力された2次元のリストから値を取り出して、引数から受け取った関数を適宜実行していく
notの場合はリストのサイズが1になるのでcaseで条件を分岐させている

def nbit_converter(inputs, gate_func) do
    Enum.map(inputs, fn row -> 
        case Enum.count(row) == 2 do
          true ->
            [x, y] = row
            gate_func.(x, y)
          false ->
            [x] = row
            gate_func.(x)
        end
      end
    )
  end

この関数を使ってnandなどをnビット対応にラップする
関数を引数で渡す場合には &モジュール名.関数名/引数の数 とすることで可能

defmodule NbitRogicalGate do
  def nbit_converter(inputs, gate_func) do
    Enum.map(inputs, fn row -> 
        case Enum.count(row) == 2 do
          true ->
            [x, y] = row
            gate_func.(x, y)
          false ->
            [x] = row
            gate_func.(x)
        end
      end
    )
  end
  def nand(inputs), do: nbit_converter(inputs, &RogicalGate.nand/2)
  def not_(inputs), do: nbit_converter(inputs, &RogicalGate.not_/1)
  def or_(inputs), do: nbit_converter(inputs, &RogicalGate.or_/2)
  def and_(inputs), do: nbit_converter(inputs, &RogicalGate.and_/2)
  def xor_(inputs), do: nbit_converter(inputs, &RogicalGate.xor_/2)
end

実行結果を見てみる

info = [
  [0, 0],
  [1, 0],
  [0, 1],
  [1, 1],
]

info_for_not = [
  [1],
  [0]
]

res = [
  NbitRogicalGate.nand(info),
  NbitRogicalGate.not_(info_for_not),
  NbitRogicalGate.or_(info),
  NbitRogicalGate.and_(info),
  NbitRogicalGate.xor_(info),
]

Enum.map(res, &(IO.inspect(&1)))

先ほどの表どおり、値が上手く出力された

[1, 1, 1, 0]
[0, 1]
[0, 1, 1, 1]
[0, 0, 0, 1]
[0, 1, 1, 0]

コンピューターで最も基本的なブール理論の論理ゲートの実装が何とか出来た
この論理ゲートの幾多なる組み合わせでコンピューターが動いていると考えると
考えた奴はマジで天才すぎる

【レポート】第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の便利さが伝われば光栄です