"Swift" vs "Swift その2" (vs Go) 戦力比較

  • 39
    いいね
  • 4
    コメント
この記事は最終更新日から1年以上が経過しています。

明日から Qiita Advent Calendar 2015 が始まります。

先日 @takoratta さんからアナウンスがあったように、なんと今年は Advent Calendar のランキングが発表されるようです!ランキングは、「購読者数」と「総ストック数」の二つでランク付けされるようです。せっかくのお祭りなので盛り上がって楽しみたいですね😃

そこで気になるのが Swift の二つのカレンダーの対決です。 Swift には現在、 @shimesaba さんの一人カレンダーを除いて、二つのカレンダーがあります。

普通に考えれば "その1" が有利なんですが、 "その1" が募集開始 3 時間ほどで埋まってしまったため、多くの人が "その1" から漏れてしまいました。そのため、 "その2" のメンバーもかなり充実しています。

たとえば、↓は Swift タグのストック数ランキング ですが、 1 位の @susieyy さん、 3 位の僕、 9 位の @g08m11 さんが "その2" に参加しているのに対して、 "その1" 参加者で TOP 10 入りしているのは 2 位の @mono0926 さん一人です。

  1. @susieyy (その2)
  2. @mono0926 (その1)
  3. @koher (その2)
  4. @nori0620 (不参加)
  5. @dankogai (不参加)
  6. @mochizukikotaro (不参加)
  7. @merrill (不参加)
  8. @Night___ (不参加)
  9. @g08m11 (その2)
  10. @cotrpepe (不参加)

「購読者数」では "その1" が 189 、 "その2" が 125 と大きく差を開けられていますが( 2015-11-28 03:43 時点)、「総ストック数」の勝敗はどちらに転ぶかわかりません。 特に初日の @mono0926 さんと @susieyy さんの、ランキング TOP 2 同士の直接対決は要注目です!

Qiita API + Swift スクリプトで戦力比較

それだけではおもしろくないので、 Qiita API を使って、それぞれのカレンダーの全参加者の情報を取得して戦力を分析してみました。

下記の意味でのサンプルとしてもご活用下さい。

  • Swift によるスクリプト
  • Swift による関数型プログラミング
  • Swift の各種ライブラリ(下記)の利用

各種ライブラリは次のような用途で利用しています。

  • Alamofire によるネットワークアクセス
  • Fuzi による HTML のスクレイピング
  • Argo による JSON のデコード
  • PromiseK による Promise を使った非同期処理
  • Runes の関数型プログラミングで使われる演算子( >>-, <^>, <*> など)
  • Curry によるカリー化
  • Carthage によるライブラリ管理

基本的な処理の流れは次の通りです。

  1. カレンダーのページの HTML を Alamofire で取得( PromiseK で非同期処理を Promise 化)
  2. Fuzi で HTML から参加者のユーザー ID をスクレイピング
  3. Alamofire で Qiita API を叩いて各参加者の情報を取得し Argo, Runes, Curry でデコード
  4. 得られた情報を元に戦力を計算して出力

せっかくのなので、 "Swift" と "Swift その2" だけでなく、 Swift の最大のライバル Go についても調べました。

Go のカレンダーは Swift 以上のスピードで埋まり、現段階で "その3" まで埋まっています。 Swift と Go は近年生まれた言語の中で特に人気を博している二つですが、その設計思想は大きく異なります。僕の主観では、 Swift がこれまでの言語の進化を更に推し進めて、構文と型システムを洗練させることでシンプルにまとめた言語なら、 Go はこれまでの言語の進化を見直して、それほど多用しないものはばっさりと切り捨てることでシンプルにまとめた言語です。そんな二つの言語が共に人気なのはおもしろいです。

他にも、この投稿を書いている内に続々とカレンダーが埋まって来たので、次の言語についても計測してみました。

  • Java
  • Python
  • C#
  • Haskell
  • Clojure

戦力の計算方法

戦力の計算は色々な方法が考えられますが、ここでは単純に各ユーザーの投稿の平均ストック数を求め、 25 人分の合計を使うことにします。

ただし、そのカレンダーのテーマと関係のない投稿は除外します。 Swift のカレンダーであれば Swift タグがついたものだけを対象として計算します。そうする理由は、たとえば、僕の場合 Swift 関連の投稿ではそれなりのストックがありますが、他の言語について書いたときに期待できるストック数は小さくなると考えられるなど、テーマによって期待できるストック数が異なるからです。

また、 Qiita API の関係で、一度のリクエストで 100 件までしかデータを取得できないので、(ページングは面倒ですし実質影響は小さいと思うので)最新の投稿 100 件のみを対象とします。

投稿数が 0 にユーザーについては計算ができないのでスコア上 0.0 として扱います。

なお、 Qiita API v2 ではストック数がとれなかったので Qiita API v1 を利用しています。

結果

カレンダー スコア
Swift 1042.62
Swift その2 911.093
Go 522.824
Go その2 285.48
Go その3 252.452
Java 256.362
Python 342.092
C# 103.255
Haskell 161.258
Clojure 305.476

( 2015-11-28 未明時点 )

"Swift" にはおよびませんでしたが、予想通り "Swift その2" もほとんど遜色ありません。ますます "Swift" と "Swift その2" の対決が楽しみです! "Swift" を購読してるけど "Swift その2" はまだ購読していないという人は、この機会にぜひ購読下さい

Swift が Go より高めに出ているのは、 Swift の方が Qiita で書いている人が多いということが大きいと思います。 Qiita Advent Calendar は外部ブログからでも参加できるので、 Qiita を使っている人の割合が少ないと当然スコアも低くなります。 Java や C#, Haskell は投稿数 0 の人が多かったので、外部ブログ利用者が多いのだと思われます。

このスコアは言語の優劣をつけるためのものではなくて、あくまで Advent Calendar ランキングの順位予測のためのものです。外部ブログはランキングにカウントされないようなので、外部ブログ利用者が多いカレンダーでスコアが低くなるのは正しい結果だと言えます。

スコアを見る限りは Swift が優勢なように見えますが、実際の順位は一つの投稿がバズっただけで順位はひっくり返ります。どんな結果になるか楽しみですね!

詳細

スクリプト はすべてのユーザーのスコアを出力しますが、ここでは Swift と Go の上位三人を除いて伏せておきます。興味がある人はリンク先のリポジトリを clone してスクリプトを実行してみてください。次のようにコマンドライン引数を変えれば任意1のカレンダーについて戦力を計算できます。

# 引数は <カレンダー名> <タグ名>
swift -F Carthage/Build/Mac/ -I /usr/include/libxml2 main.swift go2 go
Swift
担当日 ユーザー 総ストック数 投稿数 平均ストック数
12/1 @mono0926 4921 27 182.259
12/18 @tonkotsuboy_com 439 3 146.333
12/23 @k_kinukawa 509 4 127.25
...
戦力 1042.62
Swift その2
担当日 ユーザー 総ストック数 投稿数 平均ストック数
12/1 @susieyy 7380 30 246.0
12/10 @kazu0620 589 4 147.25
12/7 @koher 3555 28 126.964
...
戦力 911.093
Go
担当日 ユーザー 総ストック数 投稿数 平均ストック数
12/5 @naoina 198 1 198.0
12/1 @tenntenn 2176 33 65.9394
12/25 @cubicdaiya 405 10 40.5
...
戦力 522.824
Go その2
担当日 ユーザー 総ストック数 投稿数 平均ストック数
12/17 @mattn 427 7 61.0
12/18 @umisama 841 17 49.4706
12/16 @hogedigo 155 4 38.75
...
戦力 285.48
Go その3
担当日 ユーザー 総ストック数 投稿数 平均ストック数
12/9 @masahikoofjoyto 197 4 49.25
12/17 @methane 503 11 45.7273
12/24 @shibukawa 421 10 42.1
...
戦力 252.452

ソースコード

実行方法についての詳細は GitHub を御覧下さい。

エラーが発生した場合はエラーの理由は捨てて、 Optional を使ってエラーが発生したことだけを検出しています。 PromiseK の Promise はエラー処理を担当しないので、エラーはすべて Optional に任せています。もし、エラー情報を保持したければ、 ResultEither を使うこともできます。

Promise の非同期処理をチェーンして全体を一つの式にすることもできますが、可読性が悪くなりすぎるのであえて分割してます。

import Foundation

import Alamofire
import Argo
import Curry
import Fuzi
import PromiseK
import Runes

///// Alamofire を Promise 化するための拡張 /////

extension Request {
    public func promisedResponse(queue queue: dispatch_queue_t? = nil)
        -> Promise<(NSURLRequest?, NSHTTPURLResponse?, NSData?, NSError?)> {
        return Promise { resolve in
            self.response(queue: queue) { resolve(pure($0)) }
        }
    }

    public func promisedResponseJSON(options options: NSJSONReadingOptions = .AllowFragments)
        -> Promise<Response<AnyObject, NSError>> {
        return Promise { resolve in
            self.responseJSON(options: options) { resolve(pure($0)) }
        }
    }

}

///// 非同期処理中にプログラムが終了してしまわないように Promise を同期的に待たせるための拡張 /////

extension Promise {
    func wait() {
        var finished = false
        self.flatMap { (value: T) -> Promise<()> in
            finished = true
            return Promise<()>()
        }
        while (!finished){
            NSRunLoop.currentRunLoop().runUntilDate(NSDate(timeIntervalSinceNow: 0.1))
        }
    }
}

///// このスクリプトで利用するデータ型 /////

// 投稿
struct Item: Decodable {
    let tags: [String]
    let stockCount: Int

    static func decode(j: JSON) -> Decoded<Item> { // Argo のデコード用
        let tags: Decoded<[String]> = (j <|| "tags")
            .flatMap { tagJsons in sequence(tagJsons.map { $0 <| "url_name" }) }
        return curry(Item.init)
            <^> tags
            <*> j <| "stock_count"
    }
}

// ユーザー
struct User {
    let id: String
    let items: [Item]

    var stockCount: Int {
        return items.reduce(0) { $0 + $1.stockCount } // 各投稿のストック数の合計
    }

    var score: Float {
        return items.count == 0 ? 0.0 : Float(stockCount) / Float(items.count)
    }
}

///// 処理の本体 /////

// コマンドライン引数を取得
let calendarName = Process.arguments[1]
let tag = Process.arguments[2]

// 1. カレンダーのページの HTML を Alamofire で取得
let html: Promise<String?> = Alamofire.request(Method.GET, "http://qiita.com/advent-calendar/2015/\(calendarName)")
    .promisedResponse().map { response in
    switch response {
    case let (_, _, .Some(data), _):
        return NSString(data: data, encoding: NSUTF8StringEncoding).map { $0 as String }
    default:
        return nil
    }
}

// 2. Fuzi で HTML から参加者のユーザー ID をスクレイピング
let userIds: Promise<[String]?> = html.map {
    $0.flatMap { html in // nil でない場合
        // HTML 文字列を XMLDocument に変換
        try? XMLDocument(string: html)
    // XMLDocument から CSS セレクタで要素を取得
    }?.css(".adventCalendarCalendar_day .adventCalendarCalendar_author a")
        // ユーザー ID を含んだ href 属性を取得
        .map { $0.attributes["href"]! }
        // 1 文字目の "/" を除去してユーザー ID に変換
        .map { $0[$0.startIndex.successor()..<$0.endIndex] }
}

// 3. Alamofire で Qiita API を叩いて各参加者の情報を取得し Argo でデコード
let users: Promise<[User]?> = userIds >>- { $0.map { userIds in // nil でない場合
    // ユーザーの投稿を一人ずつダウンロード
    userIds.reduce(Promise([])) { users, userId in
        users >>- { usersOrNil -> Promise<[User]?> in
            Alamofire.request(Method.GET, "https://qiita.com/api/v1/users/\(userId)/items", parameters: ["per_page": 100])
                // Qiita API でユーザーの投稿一覧を取得
                .promisedResponseJSON().map { response in
                let userOrNil: [User]? = response.result.value
                    // JSON をデコードして [Item] を取得
                    .flatMap { decode($0) }
                    // [Item] から指定したタグを含まないものを除去
                    .map { items in items.filter { $0.tags.contains(tag) } }
                    // [Item] を User に変換し、連結するために [User] に変換
                    .map { items in [User(id: userId, items: items)] }
                // ダウンロード済みの [User] と連結
                return curry(+) <^> usersOrNil <*> userOrNil
            }
        }
    }
} }

// 4. 得られた情報を元に戦力を計算して出力
let end: Promise<()> = users.map {
    if let users = $0 {
        print("| 担当日 | ユーザー | 総ストック数 | 投稿数 | 平均ストック数 |")
        print("|:---|:--:|---:|---:|---:|")

        zip(1...25, users).forEach { date, user in
            print("| 12/\(date) | @\(user.id) | \(user.stockCount) | \(user.items.count) | \(user.score) |")
        }

        let score = users.reduce(0.0) { $0 + $1.score }
        print("| 戦力 | | | | \(score) |")
    } else {
        print("エラーが発生しました。")
    }
}

// 非同期処理が完了するまで待機
end.wait()

まとめ

Qiita API と Swift を使ってカレンダーごとの予測ストック数を計算してみました。

ストック数はあくまでひとつの目安にすぎないので、それで言語やカレンダーの優劣が決まるようなものではありません。それでも、今年は公式に「総ストック数」のランキングが作られるということですし、せっかくのお祭りに乗っかって楽しみましょう!



  1. ただし、 25 人そろっていないカレンダーは未対応です。一応動きますが、出力の「担当日」がずれます。