Swiftで遊んでみる
ずいぶん前にSwiftでもasync/awaitに対応したらしいので、試してみる。
昔、試しにアプリを作ってみたときはコールバック地獄だったように思う(そのせいでメモリリークしてた)けれど、それが改善されるならとても嬉しい。
新規にプロジェクトをCommand Line Toolとして作っておいた。
async/awaitで通信で取得したデータをログに出してみる
これで通信した結果をログに吐くことができる。戻り値がある場合の関数もあとで作ってみる。
func fetch() async {
let request = URLRequest(url: URL(string: "https://example.com")!)
let session = try! await URLSession.shared.data(for: request)
print(String(data: session.0, encoding: .utf8)!) // バイナリデータをテキストに変換して出力する
print(session.1) // レスポンスヘッダーとかが入っている
}
呼び出しはこう
Task {
await fetch()
}
で、実行してみたが、なんも起きない。
もしかして、バックグラウンド処理を実行する前に、プログラムが終了しているからかな?
プログラムが終了しないように待ってみる
タスクが終わるまで、メインスレッドを終了しないようにすればいいはず。
Task {
await fetch()
}
Thread.sleep(2)
に変えてみたら、動いた。
ただ、これだと、データの取得に2秒以上かかる場合にはやっぱりうまくいかないし、うまくいっても絶対に2秒かかるプログラムになってしまって、大変なので、処理が終わったら終了するようにしたい。
どうするんだろ。
Swift で処理が終わるまで待つ
これによると、DispatchGroupで処理が終わるのを待つことができるらしい。JavaみたいにTaskそのものをjoinすることはできないのかな。
ともあれ、こうすればやりたいことは実現できる。
func runOnThread(task:@escaping () async -> Void) {
let group = DispatchGroup()
group.enter()
Task {
await task()
group.leave()
}
group.wait()
}
runOnThread {
await fetch()
}
JSONを取得して、オブジェクトにマッピングしてみる。
Swiftにはstructというデータ型があるようなので、それを使うっぽい。(Kotlinのdata classみたいなものかな)
func fetch() async {
// ...
print(try! JSONDecoder().decode(TodosJson.self, from: session.0)) // JSONDecoder で data 型を struct にマッピングする
// ...
}
struct TodoJson: Decodable {
var text: String = ""
var done: Bool = false
}
struct TodosJson: Decodable {
var todos: [TodoJson] = []
}
よさそう。
ということで、ひとまず、CLIアプリケーションでasync/awaitを使用してデータの取得を行うことができた。
ハック的なやり方でなく、await処理の終了を待つようにする
こんな感じで、Javaでいうところのメインクラスを作って、そのクラスのメイン関数をasync対応すればよいらしい。これだと、 runOnThread
が不要になる。
ちなみに、これで実行しようとすると、 main.swift
というファイルだとコンパイルエラーになる。
どうやらmain.swiftは特別なファイルで、このファイルをメイン関数だと思って実行してくれるっぽい。しかし、メインクラスを書いてしまうと、メイン関数内にさらにメインクラスの定義があるぞ?ということでエラーとなるっぽい。
Swiftの正式な(?)やりかたで非同期処理を実装する方法がわかったといえそう。
APIサーバーを立てる
Vaporというフルスタック?フレームワークがあるっぽいけど、やっぱりいちからプロジェクトを設定したいので、もう少し薄いライブラリを使いたい。可能であれば、HTTP通信を受けるところからやってみたい。
で、見つけたのが、 swifterというライブラリ。パスをいい感じにパースして、ルーティングする機能もあるし、最低限のAPIサーバーを立てる機能が揃っていそう。
中を見てみたけど、ソケットを開いてHTTP通信をハンドリングする、みたいな処理が書かれていたので、同じものをいちから作るのは結構めんどくさそうと判断。これを使ってみる。
Swifterを使ってみた
let server = HttpServer()
server["/hello"] = { req in
.ok(.text("hello world"))
}
try! server.start(8080)
簡単。シンプルに書けるし、よさそう。
と、思ったのだけれど、async/awaitでもあった、プログラムが終わっちゃう問題が再燃。
こっちの場合は server.start
がasync関数なわけでもないので、 runOnThread
のような処理が使えない。また、 Thread.sleep
を使うとひとまず回避はできるのだけど、無限に待つようにしてもなんとなく正しい実装じゃない感があって気持ちが悪い。
(正しく)無限に待つようにした
どうも、イベントループ(のようなもの)の処理を実行を行うようにするには、 dispatchMain
という関数を実行すればいいらしい。実際、Swifterは内部でメインキューでHTTP通信を待ち受けているようなので、これが正解だと思う。waitInfinity
みたいな関数を作らなくてよくて助かった・・・。
try! server.start(8080)
dispatchMain()
これにて、正しく無限に待つようになった。
でも、こういうの、ドキュメントに書いてくれてもいいと思うんだけどな。本来はiOS上でサーバー建てたいみたいな需要向けらしいので、書いてなくても困らないのもまあわかる。サーバーサイドSwiftに需要ないのだろうか。
別のフレームワーク(Vapor)でこんな議論があった。英語なのでよくわかんないけど、要はアプリケーション側が dispatchMain
することを期待しているっぽい。わかりにくいからアイデアも提供している、って感じなのかな。
CLIだけでなく、iOSとかでも動作するように考慮すると、こうするのが今のところよい、という判断なのだろうか。
Swifterをやめた
Swifterは同期処理しかできないので、非同期なasync/awaitと組み合わせてハンドラを書くことができない。一応、issueにそういう声は上がってるけど、あまり対応はしてくれなそう
では逆に、async/awaitな処理を同期処理で待つことはできないだろうか?と考えたのだけど、これはこれで難しそうだった。
そこで、ちょっと別のフレームワークに移行してみることにした。
Kituraにした。JSのフレームワークっぽい感じで、とてもとっつきやすい。async/awaitには対応していないけれど、非同期でレスポンスを返すことは可能なので(response.send
を使うタイプ)、まずはこれにしてみようと思う。
Kituraもやめた
POSTのJSONボディを受けるときにstructにうまくマッピングできないのが理由。
content-typeを見て、application/jsonだったら、Dictionary型にマッピングしてくれるんだけど、そんなことするくらいだったらstructにマッピングしてほしいなと思う。Vaporならそこもいい感じにやってくれるので、Vaporを使うことにした。
この辺は実務に近いユースケースでつかってみることでわかることがありますね。
Vaporならasync/awaitにも対応しているので、とてもよい。