LoginSignup
258
157

More than 3 years have passed since last update.

Swiftのみを使って、今Qiitaを作るとしたら

Posted at

Swift は iOS アプリを作るための言語というイメージが強いと思います。しかし、実際にはサーバーサイドプログラムや機械学習、コマンドラインツールの開発など、 多様な目的で利用できる汎用言語です 。 2015 年にオープンソース化され、 Linux でも動作し、近々 Windows もサポートされる予定です。

10.png

Swift は Apple の言語ですが、それは TypeScript が Microsoft の、 Go が Google の言語だというのと同じ程度の意味しか持たないと思います。 Swift Core Team には Google のエンジニアも入っていますし、新しい言語の機能はすべて、オープンな場で議論された上で決定されます。

そんな Swift にとって期待される二つの分野が、 Web のクライアントサイドとサーバーサイドです。 WebAssembly に対応することで、今まさに、 Swift で書かれたプログラムがブラウザ上で動く ようになろうとしています。また、 サーバーサイドに関しては Swift のオープンソース化以来 4 年以上かけて進化してきました 。それらをフル活用して、 Qiita のフロントエンドもバックエンドも Swift だけで作るなら ということを考えてみたいと思います。

実際にやろうとすると 2020 年 7 月現在では茨の道ですが、実験的に考えてみるには、あえて Web アプリケーションのフロントエンドもバックエンドも Swift で作るならということを考えてみるのはおもしろい題材です。

フロントエンド

これまで、ブラウザ上で動作するプログラミング言語は JavaScript 一択でしたが1、 WebAssembly によって様々な言語がブラウザ上で動作する未来が見えてきました。

WebAssemblyは、ウェブブラウザのクライアントサイドスクリプトとして動作するプログラミング言語(低水準言語)である。wasmとも称されており、ブラウザ上でバイナリフォーマットの形で実行可能であることを特徴とする。
WebAssembly - Wikipedia

WebAssembly はモダンなウェブブラウザーで実行できる(略)ネイティブに近いパフォーマンスで動作する(略)アセンブリ風言語です。(略) C/C++ や Rust のような言語のコンパイル対象となって、それらの言語をウェブ上で実行することができます。
WebAssembly - MDN Web Docs

Swift も WebAssembly にコンパイルすることで、ブラウザ上で実行することができます。

Swift は WebAssembly の本命になり得る?

僕は、 将来的に Swift は WebAssembly の本命言語になるかもしれない と考えています。

ブラウザ上で C/C++ のコードが動作するようになっても、それらが JavaScript / TypeScript を代替するようになるのは難しいと思います。パフォーマンス上のボトルネックになっている部分を C/C++ で書くという棲み分けになるでしょう。 Rust も素晴らしい言語ですが、フロントエンド用の言語として万人受けするかと言うと疑問が残ります。

Swift はアプリ開発に用いられている言語です。言語仕様的にフロントエンドとの相性は良いでしょう。 フロントエンドエンジニアにとってはとっつきやすいライトな書き心地の言語 だと思います。

一方で、 Swift は C/C++ と遜色ないパフォーマンスを実現可能な言語 です。現時点では、パフォーマンスを追求したときに不利になる部分もありますが、 Rust のような所有権を導入することで、この問題を解決しようとしてます。 Swift 6 (おそらく 2021 年リリースになる次期メジャーバージョン)についてアナウンスされた "On the road to Swift 6" の中でも所有権について述べられており、近い内に利用可能になるのではないかと考えられます2

つまり、 Swift は WebAssembly の特徴であるパフォーマンスと、フロントエンドの書きやすさを両立できる言語だと言えます。

Swift for WebAssembly の現状

2020 年 7 月現在、 Swift は正式には WebAssembly に対応していません。しかし、昨年から今年にかけて急速に進展してきています。

Swift の WebAssembly 対応は、 SwiftWasm というプロジェクトで行われています。 SwiftWasm は Swift Core Team のアドバイスを受けながら、 Swift コンパイラを fork して WebAssembly 対応を進め、本流の Swift リポジトリにパッチを送っています。 SwiftWasm の中心人物の一人である @kateinoigakukun は、その進捗を次のように報告しています。

これによると、昨年時点では標準ライブラリのテストが 20% ほどしか通らなかったのが、 2 月時点で 90% 、 4 月についに 100% 通るようになったと書かれています。また、 Swift Package Manager ( Swift のおける npm や gem のようなものです)の WebAssembly 対応をしたり、 WebAssembly 用のデバッガを開発したり、 SwiftWasm 公式になっている Swift と JavaScript を連携するライブラリ "JavaScriptKit" を作ったり、周辺作業もとんでもない成果です。とてもインターンとは思えません。

そのような成果があり、最近ついに Swift がブラウザ上で動くようになりました。 swiftwasm.org で、実際に Swift のコードをブラウザ上で実行してみることができます。

20.png

@kateinoigakukun は現在 Google Summer of Code ( GSoC )で Swift のリンク時最適化に取り組んでいます(参考: わいわいswiftc #21)。現状の SwiftWasm では、 Swift のコードを実行するために標準ライブラリが丸ごとリンクされてしまうため、 Hello World でも 15MB ものサイズになってしまっています。実用を考えると、このままでは現実的ではありません。リンク時最適化によって利用されていない関数等を削除することで、そのサイズの大部分を削減できると考えられます。夏が終わる頃には、 SwiftWasm はさらに一歩進んでいるかもしれません。

SwiftWasm でオンラインエディタを作る

Qiita はあまり JavaScript で派手に動くタイプのサイトではありません。 SwiftWasm で Qiita のフロントエンドを作るとして、どこについて考えてみるとおもしろいでしょうか。

Qiita のページを一通り考えてみた結果、新規記事の投稿ページにおけるオンラインエディタが一番難しそうだという結論に至りました。オンラインエディタは左側に Markdown でテキストが入力されると、リアルタイムに右側にレンダリング結果を表示する必要があります。 Qiita の中で比較的動的な部分が大きいページです。

30.png

SwiftWasm でオンラインエディタを作るためには、 DOM の操作が欠かせないでしょう。前述の JavaScriptKit を使えば、次のように、 Swift で DOM を操作することができます。

import JavaScriptKit

let document = JSObjectRef.global.document.object!

let div = document.createElement!("div").object!
div.innerText = "Hello, Swift!"
let body = document.body.object!
body.appendChild!(div)

このライブラリは、 Swift が動的型付言語と連携するために導入された Dynamic Member LookupDynamic Callable を用いて実装されており、 Swift から JavaScript の関数やメソッドを呼び出したり、フィールドにアクセスしたりすることができます。そのため、 JavaScript と同じように DOM を操作することが可能です。

さて、Markdown から HTML への変換や、シンタックスハイライティングはどうすれば良いでしょうか。これは、 JavaScript の既存のライブラリの力を借りるのが良いでしょう。たとえば、 markdown-ithighlight.jsを組み合わせて実現するなどが考えられます。 JavaScriptKit のおかげで、これらのライブラリを再発明しなくても済みそうです。

JavaScript のライブラリを使うのは一見「 Swift のみ」に反するように思えるかもしれません。しかし、ここでは Qiita を作るために自分で書かなければならないコードが「 Swift のみ」であるというルールで考えます。そもそも、 Swift は glibc のような C 言語のライブラリに依存していたりします。他の言語も、自身にしか依存していないというケースは少ないでしょう。「〇〇(言語)のみ」を現実的に考えると、そのサービスを作るために自分が書くコードが「○○のみ」であるというルールは妥当に思えます。

Markdown を HTML に変換する部分については、 Ink のような Swift 製 Markdown ライブラリを使うこともできるかもしれません。前述の通り、 SwiftWasm は Swift Package Manager に対応しているので、これらのライブラリの導入も簡単です。3

SwiftWasm と仮想 DOM

フロントエンドのコードを書くに当たって、原始的に DOM が操作できるだけでは力不足です。モダンなフロントエンド開発には、 React のような道具が欠かせないでしょう。

iOS アプリや macOS アプリ開発においては、 2019 年に Apple が SwiftUI を発表しました。 SwiftUI は React の仮想 DOM のような仕組みを持つ UI フレームワークです。徐々に現場でも採用され始めており、今後 iOS アプリ開発を劇的に変えるものと考えられています。僕も、まさに今仕事で SwiftUI を使って iOS アプリを作っています。 SwiftUI の強力さを体感するには、 SwiftUI を使ってゼロからアプリを作り上げていくがおすすめです。

しかし、 SwiftUI をそのまま SwiftWasm で使うことはできません。そこで、 SwiftWasm プロジェクトの一貫として、 SwiftUI 互換を目指す Tokamak というライブラリが開発されました。

たとえば、次のようにして "Increment" と "Reset" ができるカウンターを作ることができます。このコードは import 部分を変えれば完全に SwiftUI でも動作します。

import TokamakDOM

struct Counter: View {
    @State var count: Int = 0

    var body: some View {
        VStack {
            Text("\(count)")
            HStack {
                Button("Reset") { count = 0 }
                Button("Increment") { count += 1 }
            }
        }
    }
}

この Counter は仮想的な View であり、実体は別に存在します。 @State が付与された count が書き換えられると、 body が再実行されて仮想 View が生成され、差分が実体に反映されます。

Counter は、 DOMRenderer を使うことで、 JavaScriptKit で取得/作成した DOM 要素の中にレンダリングすることができます。

let div = document.createElement!("div").object!
let renderer = DOMRenderer(Counter(), div)

また、 SwiftUI には存在しない HTML という View を使って、任意の HTML タグを用いて View を作ることもできます。

たとえば、

<div id="foo">
    <div id="bar">
    </div>
</div>

に相当する View は次のように書けます。

HTML("div", ["id": "foo"]) {
    HTML("div", ["id": "bar"])
}

これらを使えば、↓のようにして Markdown を入力するテキストエリアを作れるでしょう。

import TokamakDOM

struct Editor: View {
    let id: String
    @Binding var text: String

    var body: some View {
        HTML("textarea", ["id": id], listeners: [
            "keyup": { event in
                // textarea の value を取得して `text` に反映
            },
        ])
    }
}

バックエンド

Qiita を作るために必要なバックエンドの構成を考えてみましょう。最低限、次のようなものが必要でしょう。

  • 大量のリクエストをさばく Web サーバー
  • リクエストを Web サーバーに振り分けるロードバランサー
  • スケールするデータベース
  • 定期的にランキングを生成するバッチ処理

AWS を使うとして、 Web サーバーは EC2 インスタンスを必要な数だけ立ち上げて、 ELB でリクエストを振り分ければ良いでしょうか。

データベースには何を使うのが良いでしょう。 Qiita のデータ構造はそれほど複雑ではなさそうですが、一方で、高いスケーラビリティが要求されそうです。リレーショナルデータベースよりも DynamoDB が適しているでしょう。

僕はバックエンドエンジニアではないので自信を持ってこれが良いとは言えないですが、とりあえずそのような構成で考えてみましょう。

Web サーバー

Swift で最もメジャーな Web フレームワークは Vapor であり、 Server-Side Swift (SSS) のデファクトになっています。

40.png

Vapor を動かすのに Amazon Linux か Ubuntu かどちらがいいかの知見は持っていませんが、 Swift は Ubuntu の方がサポート状況が良さそうなので、 Ubuntu + Nginx + Vapor という構成にすることにしましょう。

Vapor を使うとサーバーサイドのコードを次のように書くことができます。 GET でリクエストを受けたときに "Hello, world." とレスポンスを返すだけのプログラムです。

import Vapor

let app = try Application(.detect())
defer { app.shutdown() }

app.get("hello") { req in
    return "Hello, world."
}

try app.run()

(引用元: https://vapor.codes/

また、 Vapor はそれ単体だけではなく、連携して動作するための様々な道具が開発されています。たとえば、 Vapor 公式の Leaf というテンプレートエンジンを使えば、次のようなテンプレートで HTML を出力させることができます。

<h1>#(title)</h1>
#for(number in numbers){
    <p>#(number)</p>
}

(引用元: https://docs.vapor.codes/3.0/leaf/overview/

その他にも Web フレームワークに求められる諸機能はそろっており、 Qiita を実装する上での障害は特にないように思えます。

データベース

Vapor には Fluent という ORM フレームワークがあります。これを使って MySQL や PostgreSQL などにアクセスすることができます。

たとえば、 Qiita の記事を表すモデルクラスを作って、

final class Article: Model {
    static let schema = "articles"

    @ID(key: .id)
    var id: String?

    @Field(key: "name")
    var title: String

    ...
}

次のようにしてデータベースを書き込むことができます。

app.post("articles") { req -> EventLoopFuture<Article> in
    let article = try req.content.decode(Article.self)
    return article.save(on: req.db).map { _ in article }
}

DynamoDB についても、 Vapor 3.0 のドキュメントには非公式の Community Drivers として FluentDynamoDB挙げられています。しかし、最新の Vapor 4.0 のドキュメントでは記述が消えており、また、リポジトリのスターも 4 と、これを使うのも不安が残ります。

DynamoDB にアクセスするには、 Fluent の利用は諦めて AWS SDK Swift を使った方が無難でしょう。僕自身はやったことはないですが、調べてみたところ、 DynamoDB 型の putItem メソッド

func putItem(_ input: PutItemInput, on eventLoop: EventLoop? = nil) -> EventLoopFuture<PutItemOutput>

を使って、次のような感じで記事を書き込むことができそうです。

let dynamoDB: DynamoDB = ...

let item: [String: DynamoDB.AttributeValue] = [
    "title": .s(article.title),
    "tags": .ss(article.tags),
    ...
]
let input = DynamoDB.PutItemInput(item: item, tableName: "articles")
let output = dynamoDB.putItem(input)

↑のコードでは、 item を手で(人間がコードを書いて)作っていますが、 CodableSwift Encoders)を活用すれば、この変換を自動化することができるでしょう。

これで、 Swift から DynamoDB の利用もできました。

タイムラインの生成

Qiita にはフォローとタイムラインの仕組みがあります。タイムラインの実現はデータ構造上、少々やっかいです。一人のユーザーは何百・何千ものユーザーをフォローし得ます。リクエストを受ける度にフォローグラフを元にリアルタイムに記事を JOIN して、しかも時系列順に並べて提示するというのは非現実的でしょう。現実的には、各ユーザーのタイムラインをデータとして保持し、記事が投稿されたらフォロワーのタイムラインにもデータを書き込むなどの設計にする必要があるでしょう4

そうすればリクエストごとに負荷の高い JOIN をしなくて済みます。しかし、記事が投稿されたときには投稿者のフォロワー全員のタイムラインに書き込みが必要です。フォロワーは何百・何千人になるかもしれないので、 1 件の記事の投稿が何百・何千件もの書き込みを必要とするかもしれないということです。

フォロワーが一桁というユーザーが大半でしょうから、一部の人気ユーザーの投稿でそのような大量の書き込みが発生しても、全体の負荷としては誤差の範囲かもしれません。しかし、投稿時に何百・何千件もの書き込みを Web サーバーから行うのは避けたいところです。 AWS Lambda を使って、 DynamoDB への書き込みをトリガーとして タイムラインを生成することにしましょう。

先日( 2020 年 5 月)、 Swift AWS Lambda RuntimeSwift の公式ブログからアナウンスされました。 Swift AWS Lambda Runtime を使った Lambda のコードは次のように書くことができます。

// Import the module
import AWSLambdaRuntime

// in this example we are receiving and responding with strings
Lambda.run { (context, name: String, callback: @escaping (Result<String, Error>) -> Void) in
  callback(.success("Hello, \(name)"))
}

(引用元: https://github.com/swift-server/swift-aws-lambda-runtime/

これを使えば、タイムラインを生成する Lambda のコードも Swift で書くことができそうです。

ランキングの生成

残すはランキングの生成です。アルゴリズムの詳細はわかりませんが、おそらく機械学習を使っているでしょう。

Swift for TensorFlow

Swift と機械学習を語る上で欠かせないものに、 Swift for TensorFlow があります。

Swift for TensorFlow は Google が進めているプロジェクトで、 Swift の生みの親である Chris Lattner が Google 移籍後に立ち上げたものです。 Swift for TensorFlow はとてもおもしろいプロジェクトで、ただの Swift 向け TensorFlow ラッパーではありません。それなら TensorFlow for Swift ( Swift のための TensorFlow)になるはずです。 TensorFlow のための Swift 、つまり、 Swift for TensorFlow は TensorFlow のために fork された Swift コンパイラ です。

Swift for TensorFlow - TFiwS (TensorFlow Dev Summit 2018)

Swift for TensorFlow については上記ビデオと公式ドキュメントで詳しく解説されていますが、簡単に説明すると次のような課題を解決するものです。

Deep Learning フレームワークには、主に Define-and-Run と Define-by-Run の二つの方式があります。 Define-and-Run では最初に(ニューラル)ネットワークを定義し、それからデータが流されます。一方、 Define-by-Run では動的にネットワークを定義します。そのため、条件分岐やループのような制御構文を組み合わせてネットワークを定義することが容易です。パフォーマンスでは Define-and-Run に利がありますが、コードの書きやすさでは Define-by-Run に軍配が上がります。元々 Define-and-Run を採用していた TensorFlow も TensorFlow 2.0 で Define-by-Run をデフォルトにするなど、近年では Define-by-Run が優勢です。

この、 Define-and-Run と Define-by-Run のいいとこ取りをしようというのが Swift for TensorFlow です。 Swift コンパイラに手を入れて、コンパイル時に制御フローやネットワークを解析できるようにすることで、静的にネットワークを構築しながら、 Define-by-Run の書きやすさを実現しようというエキセントリックなプロジェクトです。

また、ニューラルネットワークライブラリに欠かせない自動微分をコンパイル時に行う機能を生み出し、それを Swift の本流に反映させようとしています。この提案は受け入れられ、将来的には Swift に取り込まれる予定です。

さらに、より長大かつ野心的なゴールも見据えているようです。 Swift for TensorFlow はコンパイラの処理に Graph Program Extraction という新たなステージを追加し、コードを解析して TensorFlow のグラフ(棒グラフ等のグラフではなく、グラフ理論のグラフ(ツリーなどを含むデータ構造))を構築します。これを応用すれば次のようなことも可能だと書かれています。

Finally, while TensorFlow is the reason we built this infrastructure, its algorithms are independent of TensorFlow itself: the same compiler transformation can extract any computation that executes asynchronously from the host program while communicating through sends and receives. This is useful and can be applied to anything that represents computation as a graph, including other ML frameworks, other kinds of accelerators (for cryptography, graphics, transcoding, etc), and general distributed systems programming models based on graph abstractions. We are interested in exploring new applications of this algorithm in the future.

(参考訳)最後に、私達がこのインフラを構築したのは TensorFlow のためですが、そのアルゴリズムは TensorFlow 自体からは独立しています。同様のコンパイラによる変換は、送受信を介して通信しながらホストプログラムから非同期的に実行される任意の計算を抽出することができます。これは、グラフとして表すことができる任意の計算に適用し、役立てることができます。そのような計算の例としては、他の機械学習フレームワークや、その他のアクセラレータ(暗号化やグラフィック処理、トランスコードなど)、グラフの抽象化に基づく一般的な分散システムプログラミングモデルなどが挙げられます。私達は、将来的にこのアルゴリズムを適用できる新しい問題を見つけ出すことに関心があります。

Graph Program Extraction - Swift for TensorFlow Design Overview

壮大すぎてくらくらしますが、とても興味深いですね。

この Swift for TensorFlow を使って機械学習をすれば、 Swift だけを使って Qiita のランキングを生成することもできそうです。

Core ML / Create ML

別の手段としては、ランキング生成だけは macOS 上で行い、 Core ML / Create ML の力を借りるということもできます。その場合、 Mac サーバーを用意し、ランキング生成だけそちらで行います。

Core ML は Apple のチップ上で高速にニューラルネットワークの計算することができるライブラリです。 2017 年にリリースされた A11 チップ以降、最近の iPhone や iPad 、これから発売されるだろう Apple Silicon 搭載の Mac には、 Neural Engine と呼ばれるニューラルネットワークの計算のためのハードウェアが搭載されています。これによって、高速かつ高精度な顔認識などが支えられています( Neural Engine が搭載されていなくても、 Core ML は GPU 等を用いて適切に処理を行います)。

また、 Core ML は OS に内蔵されているため、ニューラルネットワークのためのライブラリをアプリに埋め込まなくて良いのもうれしいところです。ちょっと機械学習の成果を使うためだけに、巨大な機械学習ライブラリを埋め込まないといけないのは避けたいでしょう。

Apple の Core ML Tools を用いて TensorFlow 等で構築したモデルを Core ML 用に変換することができます。また、 Apple が Core ML 用に公開しているモデルを使えば、すぐに機械学習を試すこともできます。

50.png

Core ML には学習のための機能は備わっていませんが、 Create ML を用いることで Core ML 用のモデルを生成することができます。

Create ML は一般的な Deep Learning フレームワークとは異なり、自身でネットワークを定義することはできません。入力と出力のペアをデータとして用意すれば、あとは自動的にネットワークが構築され、いい感じに学習してくれます。たとえば、画像の識別であれば、 各クラスごとに最小 10 枚の画像を用意して与えるだけで識別器を生成してくれます 。おそらく、内部で転移学習かファインチューニングを行っているのでしょう。汎用的な Deep Learning フレームワークとするのではなく、一般のエンジニアが手軽に機械学習の恩恵を受けられるツールを提供するという選択が、僕はいかにも Apple っぽいなと思いました。

さて、ランキングを生成するに当たっては、Creating a Model from Tabular Dataが参考になりそうです。これは、画像や動画などではなく、表形式のデータからモデルを訓練するサンプルです。記事のランキングを作るには記事にスコアを付けてそれを予測するモデルを訓練する必要があります。本文を学習するのではなく、 LGTM の数や投稿時刻、直近の LGTM (加速してるか減速してるか)、投稿したユーザーのスコアなどデータを元にスコア付けを行う場合、データはこのような表形式のものとなります。

学習を行うコードは、次のようにして Swift で書くことができます。

// CSV からデータを読み込み
let csvFile = Bundle.main.url(forResource: "MarsHabitats", withExtension: "csv")!
let dataTable = try MLDataTable(contentsOf: csvFile)

// 価格予測に必要なカラムだけを抽出
let regressorColumns = ["price", "solarPanels", "greenhouses", "size"]
let regressorTable = dataTable[regressorColumns]

// 訓練データとテストデータに分割
let (regressorEvaluationTable, regressorTrainingTable) = regressorTable.randomSplit(by: 0.20, seed: 5)

// 学習
let regressor = try MLLinearRegressor(trainingData: regressorTrainingTable, targetColumn: "price")

// 評価
let regressorEvalutation = regressor.evaluation(on: regressorEvaluationTable)

(引用元: https://developer.apple.com/documentation/createml/creating_a_model_from_tabular_data , コメントは著者が付与)

これと同じように記事のスコアを予測する回帰の問題としてモデルを訓練し、得られたモデルを使ってランキングを生成することができます。

なお、ここでは詳細には触れませんが、本文のテキストを元にスコアリングしたい場合は、テキストを入力とするモデルを訓練することもできます。

まとめ

Qiita を作るために必要となりそうな技術要素について、 Swift だけで実現するにはどうすれば良いかを検討してみました。まだまだ開発段階のものも多く、実用には様々なハードルがあるでしょう。しかし、 Swift で様々な領域のコードを書く未来を垣間見ることができたのではないかと思います。 Swift は現在も急速に進化を続けており、 iOS アプリ以外の実プロダクトでも Swift が使える未来が楽しみです!

⚠️ 本記事は、普段使わない技術ばかりを挙げて書いたので、おかしなところがあるかもしれません。マサカリ歓迎です!


  1. 過去には Java Applet や FLASH の ActionScript などもありましたが、 2020 年現在の現実的な選択肢ではないでしょう。 

  2. 一部の機能は _modify などとして、 2020 年 7 月現在でも、すでに試験的に利用可能になっています。 

  3. ただし、本当に SwiftWasm で Ink をビルドできるかは試していません。 

  4. そういうシステムを作ったことがあるわけではないので、本当にこの方法が良いのかはあまり自身がないです。もっと良い方法があるかもしれません。 

258
157
6

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
258
157