Swift製webアプリケーションフレームワーク Vapor を読むやつ #1

Vapor とは SSS(Server Side Swift) のwebアプリケーションフレームワークです。

docs: https://docs.vapor.codes/3.0/

セットアップ

CLI ツールの使用を推奨しているので brew でいれておく。プロジェクトの作成は vapor new で。Swift 初心者過ぎてよくわかっていないが XCode で開きたい場合は vapor xcode すると必要なファイルができるっぽい。

$ brew install vapor/tap/vapor 
$ vapor new HelloVapor # HelloVapor ディレクトリができる
$ cd HelloVapor
$ vapor xcode

この時点でのフォルダはこんな感じ。最初から git 管理されているので git ls-files で。

$ git ls-files
.gitignore
Package.resolved
Package.swift
Public/.gitkeep
README.md
Sources/App/Controllers/.gitkeep
Sources/App/Controllers/TodoController.swift
Sources/App/Models/.gitkeep
Sources/App/Models/Todo.swift
Sources/App/app.swift
Sources/App/boot.swift
Sources/App/configure.swift
Sources/App/routes.swift
Sources/Run/main.swift
Tests/.gitkeep
Tests/AppTests/AppTests.swift
Tests/LinuxMain.swift
circle.yml
cloud.yml

vapor new 時に --template (あるいは --web, --auth もしくは --api ) を指定することで設定可能です。以下のようなオプションがあることから単純に git clone してきているのだということがなんとなく推測できます。

       --branch An optional branch to specify when cloning
          --tag An optional tag to specify when cloning

template を作るときはコミットは squash して1コミットにしておいたほうがよさそうですね。

XCode から run して動かしてみます ( https://docs.vapor.codes/3.0/getting-started/xcode/ )

以下適当に動作確認。

$ open http://localhost:8080/hello/
$ curl localhost:8080/todos/
[]
$ curl -XPOST -d "title=1" localhost:8080/todos/
{"id":1,"title":"1"}
$ curl localhost:8080/todos/
[{"id":1,"title":"1"}]
$ curl -XPOST -d '{"title": "2"}' localhost:8080/todos/
{"error":true,"reason":"Value of type 'String' required for key ''."}
$ curl -XPOST -d '{"title": "2"}' -H 'Content-Type: application/json' localhost:8080/todos/
{"id":1,"title":"2"}
$ curl localhost:8080/todos/
[{"id":1,"title":"1"},{"id":2,"title":"2"}]

とりあえずいわゆるふつーのAPIサーバが立ってますね。

テンプレートを読む

デフォルトでは api テンプレートなのでそれにします。

https://github.com/vapor/api-template

エントリポイントっぽいところから。

/// Sources/Run/main.swift
import App

try app(.detect()).run()

Swift 初心者的には .detect() がキモいのですが、これは implicit member expression という書き方のようです。今回の場合は Environment.detect() の省略記法。 https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Expressions.html#//apple_ref/doc/uid/TP40014097-CH32-ID394

app 関数の定義が Sources/App/app.swift にあります。

/// Sources/App/app.swift
import Vapor

/// Creates an instance of Application. This is called from main.swift in the run target.
public func app(_ env: Environment) throws -> Application {
    var config = Config.default()
    var env = env
    var services = Services.default()
    try configure(&config, &env, &services)
    let app = try Application(config: config, environment: env, services: services)
    try boot(app)
    return app
}

ここから概念として Config, Service(s) が存在すること、 configure, boot を経て Application のインスタンス app が初期化されて返っていることがわかります。

configure から順に見ていきます。

/// Sources/App/configure.swift
import FluentSQLite
import Vapor

/// Called before your application initializes.
public func configure(_ config: inout Config, _ env: inout Environment, _ services: inout Services) throws {
    /// Register providers first
    try services.register(FluentSQLiteProvider())

    /// Register routes to the router
    let router = EngineRouter.default()
    try routes(router)
    services.register(router, as: Router.self)

    /// Register middleware
    var middlewares = MiddlewareConfig() // Create _empty_ middleware config
    /// middlewares.use(FileMiddleware.self) // Serves files from `Public/` directory
    middlewares.use(ErrorMiddleware.self) // Catches errors and converts to HTTP response
    services.register(middlewares)

    // Configure a SQLite database
    let sqlite = try SQLiteDatabase(storage: .memory)

    /// Register the configured SQLite database to the database config.
    var databases = DatabasesConfig()
    databases.add(database: sqlite, as: .sqlite)
    services.register(databases)

    /// Configure migrations
    var migrations = MigrationConfig()
    migrations.add(model: Todo.self, database: .sqlite)
    services.register(migrations)

}

FluentSQLite が ORM ですね。Vapor の内製ライブラリです。middleware の作り方も気になるので後で見ておきましょう。
マイグレーションは明示的に migrations.add してますけど、これ増えたときどうするんですかね。毎回増やすのかな。アノテーションとかでしれっと書けてほしいような1

マイグレーション自体は起動時に勝手にやってくれるようです。

[ INFO ] Migrating 'sqlite' database (FluentProvider.swift:28)
[ INFO ] Preparing migration 'Todo' (MigrationContainer.swift:50)
[ INFO ] Migrations complete (FluentProvider.swift:32)
Running default command: ...

具体的には try Application の中でやっているっぽい。ソース的にはここ。 https://github.com/vapor/vapor/blob/master/Sources/Vapor/Application.swift#L126-L140

今日はここまで。次回boot以降を読みます。

ハマったとこ

sqlite3 の path 問題

sqlite3 の .file("path") で path はディレクトリを含むと困ることがある。sqlite3側は mkdir などはしてくれないのでこちらでしてやる必要があるが、xcode の debug build が動くのは普段のプロジェクトとは違う場所なので mkdir db; touch db/.gitkeep とかしても無駄。

レポジトリ眺めてたらあとでこういう issue を発見した。 https://github.com/vapor/fluent-sqlite/issues/5

カラム追加

一回マイグレーションが動いたあとにモデルに属性を追加しても add_column とかはしてくれない。まあそりゃそうだ。どうなってるのが理想的なんだろう。


なお本記事は以下のエントリに触発されて書きました。

https://fiveteesixone.lackland.io/2018/05/06/addicted-to-vapor/


  1. Swift 的には attribute

macOS 10.13.3 -> 10.13.4 のアップデートをするとSEGVするやつ

概要

朝起きたら macOS のアプデがかかったらしくログインを要求された。
いつも通りユーザーを選択しパスワードをいれて FileVault の解除を待つと見たことない画面へ。
「アプデ適用に失敗しちゃったっぽいよてへぺろ とりあえず再起動する?」と聞かれたので再起動するが症状が改善しない。

TL;DR

  1. cmd + option + R を押しながら起動
  2. wifi を選択して接続、アプデ内容をDL
  3. OS再インストール

NOTE: この操作によりデータが消えたりはしてないです

試したこと

再起動

無限ループ

セーフモード + アプデ再適用

セーフモードで起動すると App Store には「OSアプデがあるよ」と出ている。ので適用。
症状改善せず。

リカバリモードで起動

cmd + R のやつ。アプデ内容が落ちてこなくて終わり。

mcd + option + R のやつ。上述の通りなんとかなった。

参考情報

症状としては下記エントリと同じ。

tidbits.com

SEGVしてるログが出る。
最終的に "failed to write private file" 的なログが出てるのがわかる。

ご参考までに。

Python3 script を雑に heroku で動かせるようにする

手元で動かしてた python スクリプトを web サービス化したときの備忘録です。
なお python 歴はゼロから deep learning 作ったくらいです。

モチベーション

秘伝の python script があった。
手元で動かすのがめんどくなったのでどうにかしたかったが、某サーバー群で python 動かすのとかがダルかったので heroku とかに適当にホストしておきたくなった。

要件は、heroku で動くこと、python3 であること、軽く認証っぽい何かがあることくらい。

実装

misc/python_simple_server.py at master · hkdnet/misc · GitHub

だいたい書いてある通り。 do_GET 潰すの忘れてデプロイしたら普通にディレクトリ見えてビビったのでマジで注意。
検証してないけど bind アドレスを忘れるとかもありそうである1

requirements.txt はレポジトリルートに置いといたら勝手に読んでくれた。助かる。

Procfile はこんな感じ。heroku は 2018-05-02 現在デフォでpython3なので特に気にしなくてよい。

web: python server.py

雑に検証した感じがこれ

$ curl localhost:8000
$ curl -XPOST -d "foo=bar" localhost:8000
$ curl -XPOST -H "Authorization: Bearer YOUR_TOKEN" -d "foo=bar" localhost:8000

# サーバー側のログ↓
127.0.0.1 - - [03/May/2018 04:44:15] "GET / HTTP/1.1" 400 -
127.0.0.1 - - [03/May/2018 04:44:26] "POST / HTTP/1.1" 400 -
127.0.0.1 - - [03/May/2018 04:44:41] "POST / HTTP/1.1" 200 -

蛇足

  • virtualenv -p python3 . で環境つくっておく
  • pip install autopep8autopep8 -i foo.py しとく

  1. 最初から '0.0.0.0' 指定したからそれで動かないのか自信がないが

TaPL読書録 #4

前回:

hkdnet.hatenablog.com

今回は5章終わりまで、と思っていたが 5.2. の最後まででいい時間になってしまったので終わり。

今回は記憶がおぼろげなのでおぼろげに書きます。

ラムダ計算におけるリストの表現などについて Scott encoding, Church encoding と言われて何もわからなかったが、以下のブログに詳しい。

d.hatena.ne.jp

ラムダ計算ができる言語が欲しくなるが、arrow function があるJavaScriptがよろしいのではないかという話になった

github.com

power の話になったときに、回答例の power2 がこれは確かに power であるという確信がもてない。ここで power2' について考える。

power2  = \n. \m. m n;
power2' = \n. \m. \s. \z. m n s z; # イータ変換による

型のようなナニカを雰囲気で想像したときに(要は定義をせずに使う)。
まず Church数 は2引数関数であり、第一引数に a -> a な関数を、第二引数に a を取り、a を返します。

cn :: (a -> a) -> a -> a

ここで m n に注目すると m :: (a -> a) -> a -> an :: (a -> a) -> a -> a について、 A = (a -> a) とすると n :: A -> A と捉えることも可能で型が一致しているような雰囲気があって面白い。

m :: (A -> A) -> A -> A
m :: ((a->a) -> (a -> a)) -> (a -> a) -> (a -> a) # 展開した
m n :: (a -> a) -> (a -> a) # nを適用した
m n :: (a -> a) -> a -> a # カリー化を考えると括弧に意味がない気がするので消した

となり m n が Church 数の型と一致するのが確認できる。

さて、我々は plus を手に入れ times を手に入れ power を手に入れるに至った。そうすると Knuth の矢印表記についても考えたくなるのは必然であるという話になった。ので、軟弱な私は即座にググった。

mindsarentmagic.org

やっぱりあったね。相性いいね、という気持ちになって終わった。

equal について、回答例では再帰を使わなかったが以下のようにも書けるような気がする。

eq = \m. \n. test (iszro m) (iszro n) (eq (prd m) (prd n));
eq = \m. \n. test (or (iszro m) (iszro n)) (and (iszro n) (iszro m)) (eq (prd m) (prd n))
2引数をとる関数に対応するYコンビネータ G に関して
equal = G eq

ここで教えてもらったのが、不動点コンビネータを使わずに関数を引数にとる方法もある。 lambda計算においては、適用するときに同じ項をそこに平べったく書けば良いとのこと(未検証)

fact f c = if (iszro c) then (succ 0) else (f f (prd c))
showFact = fact (if (iszro c) then (succ 0) else (f f (prd c))) c4

次回は5.3 〜 6章全体まで。

『わかばちゃんと学ぶ Googleアナリティクス』の献本をいただきました

f:id:hkdnet:20180422143933j:plain

はいどうも、 ハイクオリティ神レビューア のはくどーです。
頂いたの3月末なのですが……。遅くなりましてすみません。

感想

ざっくりGAの使い方が紹介されていてよいんじゃないでしょうか。アクセス解析にまつわる諸概念についてもざっと紹介され、各概念を混同しないように説明しようという意思を感じました。
また、いつもの湊川節で「どうしてそれやるんだっけ?」「それってほんとにちゃんと目標に沿ってるんですか?」と優しく詰められるのが心地よかったです。

経緯

Twitterすごい(こなみかん

ふりかえり

実際にやってたこと

GitHub ベースでのレビューでした。
レポジトリが用意されて、markdown原稿がPRでとんでくるのでレビュー。
おそらくレビューが終わったあとに出版社の人が組版してくれて本になっている。
僕がレビューしてる段階ではあくまで markdown ( + そこから生成されるHTML )が対象。

よかった

  • 対象ドメインがわからないので最初の読者として質問しまくれた
    • 結果、わかりにくい点が減ったと思う
  • はじめて用語の統一などを意識して文章を構築する経験ができた
    • ふだんは適当なので……
  • 文章だけでなく、マンガ・イラスト部分にまで足を伸ばせた

わるかった

  • PRが巨大になりがちで難しい
    • chapter 単位でのPRになっていたのだけど、分量が多い
  • 修正が妥当かどうかを確認するのが難しかった
    • 特になんどもすり合わせをしているようなとき、outdated な変更と見比べるのが難しかった
  • GitHub の diff viewer で画像が見れないのがめんどい
  • うまく違和感が言葉にできないことがあった
    • なんかよくわからないんだけどこの言い回し変な気がするんだけど俺の語彙力がないせいなのかはたして、みたいなやつ
    • いつもの仕事だったら適当に対面で話すんだけどどうしたもんか

次あったらどうしようか

  • 問題: PRが巨大になりがち
    • 解決策: chapter 単位ではなく section 単位でPRにする
      • 懸念: 用語の統一とか section 間での流れの悪さに気づけるか?
      • feature ブランチで chapter を作って各 section を feature 向けのPRとする、とよさそう
      • 今回はこういうブランチ運用とかまでちゃんと話せなかったなーという追加反省
  • 問題: 修正が妥当かわかりにくい
    • 解決策: PRブランチへのPRで修正を反映する
      • 懸念: ちょっとめんどくさいかも

2018年04月24日 00:25頃追記 図を書きました

f:id:hkdnet:20180424002556j:plain

草活1周年、に失敗した話

f:id:hkdnet:20180422141206p:plain

3/31なんですけど、とあるレポジトリにコミットしてたのが、とある事情によりレポジトリごと消えたので草も一緒に消えてしまいましたとさ……。

これ private repo のも含めていて(= 会社のも含んでいる)、OSSっぽいことをしているわけではないのでアレなんですけどね。
達成したら記念に日記書こうと思ってたのだけど達成できなかった記念に置いておきます。