やわらかテック

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

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

【レポート】第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で展開します