書いて覚えるSwift入門

第25回Anything is nothing

Swift on FreeBSD

本題に入る前にニュースを1つ。実はFreeBSDでもSwiftは動きます。ports(FreeBSDのパッケージ管理システム)にlang/swiftが登場していたことはFreeBSD Journal 2016年11/12月号にも取り上げられていたのですが、正直きちんとメンテナンスされているとは言えず、本記事執筆現在でもビルドエラーで止まってしまいます。

しかしビルドエラーの内容を精査してみると、問題はSwiftのソースコードではなく、ビルドが依存しているツールがアップデートされているのにそれにあわせてportsが更新されていないのが原因と判明したので、その点を微修正してビルドした後パッケージ化したものを公開しました

同パッケージはもともとevalparkのためにビルドしたものです。evalparkというのは任意のシェルスクリプトをフルセットのFreeBSD 11で実行してJSONで返すというすごーいWeb APIで、これだけでまるまる別記事を書けてしまうのですが、それはともかく同サービスを使うとWeb上でSwift on FreeBSDを実行できます図1⁠。

図1 evalpark
図1 evalpark

FreeBSD上で実行している証拠に、次のコードはちゃんと⁠I'm running on FreeBSD⁠と出力します。

#!/usr/local/bin/swift
var os = "(mac¦i¦tv¦watch)OS"
#if os(FreeBSD)
os = "FreeBSD"
#elseif os(Linux)
os = "Linux"
#endif
print("I'm running on \(os).")

Swiftのバージョンは1世代前の2.2.1ですが、本連載のコードはほとんどそのまま動くので、読者のみなさんもお気軽にお試しください。

Anything is nothing

それでは本題。前回の終わりで筆者はAnyは避けるべきです。少なくとも、動的言語の変数のように使うべきでありません」と述べましたが、なぜなのでしょうか?

一言で言えば、Anyには何でも入るが何もできないから」ということになります。次のとおり、確かに何でも入ります。

var a:Any
a = true
a = 42
a = 42.195
a = "Everything"
a = [true, 42, 42.195, "Everything"]
a = ["answer":a]

しかし、そのままでは何も使えません。たとえば最後の状態でa["answer"]としても、出てくるのは[true, 42, 42.195, "Everything"]ではなく`error: type 'Any' has no subscript membersというエラーだけです。⁠期待どおり」の結果を出してもらうには、(a as! [String:Any])["answer"]と型を指定しなければならないのです。これでは型を省略したことにはなりませんよね。

メモリ消費量の点からも、Anyは避けるべきです。C言語のvoid *やObjective-Cのidとは異なり、Anyには本来の値に加えて型情報も収納されています。それゆえ安全なのですが、それゆえ余計な情報も抱え込んでいることになります。

MemoryLayout.size(ofValue: 0) // 8
MemoryLayout.size(ofValue: "") // 24
MemoryLayout.size(ofValue: [0]) // 8
MemoryLayout.size(ofValue: ["":0]) // 8
MemoryLayout.size(ofValue: 0 as Any) // 32

それでもAnyという「無能な万能型」がわざわざ用意されているのには理由があります。たとえばSwift 3のFoundationのJSONSerialization.jsonObjectの戻り値はAnyですが、これによりBoolもDoubleもStringもArrayもDictionaryもすべてカバーできます。が、その分何を取り出すにも`as`しなければならずとても不便です。

JSON型を作ってみる

そんなときはどうすればよいか。もうおわかりですね。型を作ってしまえばよいのです。JSONならばこんな感じですか。

enum JSON {
    case JSNull
    case JSBool(Bool)
    case JSNumber(Double)
    case JSString(String)
    case JSArray([JSON])
    case JSObject([String:JSON])
}

こんな感じで初期化できます。

let json0:JSON = .JSObject([
    "null":.JSNull,
    "bool":.JSBool(false),
    "number":.JSNumber(0),
    "string":.JSString(""),
    "array":.JSArray([
        .JSBool(true),
        .JSString("string"),
        .JSNumber(42.195)
        ]),
    "object":.JSObject([:])
])

が、こんなの筆者だって使いたくありません。Any?から初期化できるようにしましょう。Anyではなく、Any?であるのはJSON(nil)JSON.JSNullを返したいから。

extension JSON {
    init?(_ any:Any?) {
        switch any {
        case nil:
            self = .JSNull
        case let number as Double:
            self = .JSNumber(number)
        case let int as Int:
            self = .JSNumber(Double(int))
        case let bool as Bool:
            self = .JSBool(bool)
        case let string as String:
            self = .JSString(String(string))
        case let array as [Any?]:
            self = .JSArray(
                array.map{ JSON($0)! }
            )
        case let dictionary as [String:Any?]:
            var object = [String:JSON]()
            for (k, v) in dictionary {
                object[k] = JSON(v)!
            }
            self = .JSObject(object)
        default:
            return nil
        }
    }
}

これで、

let json1 = JSON([
    "null":nil,
    "bool":false,
    "number":0,
    "string":"",
    "array":[true,"string",42.195],
    "object":[:]
] as [String:Any?])

という具合にずいぶんとスッキリしました。Any?で初期化できるようになったので、前述のJSONSerialization.jsonObjectを使えば文字列からも初期化できます。やってみましょう。

import Foundation
extension JSON {
    init?(_ s:String) {
        guard let nsd = s.data(
            using:String.Encoding.utf8
            )
            else { return nil }
        guard let any
            = try? JSONSerialization
                .jsonObject(with:nsd)
            else { return nil }
        self.init(any)
    }
}

実行結果は次のとおり。

let json2 = JSON("{\"number\":0,\"null\":null,
\"object\":{},\"array\":[true,\"string\",42.19
5],\"bool\":false,\"string\":\"\"}")

確かにできました。しかしこうしてできたJSON型の変数をprintすると、

JSObject(["number": JSON.JSNumber(0.0),
"null": JSON.JSNull, "object": JSON.
JSObject([:]), "array": JSON.JSArray([JSON.
JSBool(true), JSON.JSString("string"), JSON.
JSNumber(42.195)]), "bool": JSON.
JSBool(false), "string": JSON.JSString("")])

……という具合で、見づらいうえに使えません。逆変換もサポートしましょう。

extension JSON : CustomStringConvertible {
    var description:String {
        switch self {
        case .JSNull:
            return "null"
        case let .JSBool(b):
            return b.description
        case let .JSNumber(n):
            return n.description
        case let .JSString(s):
            return s.debugDescription
        case let .JSArray(a):
            return "["
                + a.map{ $0.description }
                   .joined(separator:",")
                + "]"
        case let .JSObject(o):
            var ds = [String]()
            for (k, v) in o {
                ds.append(
                    0k.debugDescription
                        + ":"
                        + v.description
                )
            }
            return "{"
                + ds.joined(separator:",")
                + "}"
        }
    }
}

これでJSONと文字列の相互変換はバッチリです。が、まだまだ使いづらい。値も取り出せるようにしましょうリスト1⁠。

リスト1 JSON型のサンプルに改良を加える
extension JSON {
    var isNull:Bool? {
        switch self {
            case .JSNull:      return true
            default:           return false
        }
    }
    var asBool:Bool? {
        switch self {
        case let .JSBool(b):   return b
        default:               return nil
        }
    }
    var asNumber:Double? {
        switch self {
        case let .JSNumber(n): return n
        default:               return nil
        }
    }
    var asString:String? {
        switch self {
        case let .JSString(n): return n
        default:               return nil
        }
    }
    var asArray:[JSON]? {
        switch self {
        case let .JSArray(a):  return a
        default:               return nil
        }
    }
    var asObject:[String:JSON]? {
        switch self {
        case let .JSObject(o): return o
        default:               return nil
        }
    }
    subscript(i:Int)->JSON {
        switch self {
        case let .JSArray(a):
            return i < a.count ? a[i] : .JSNull
        default:
            return .JSNull
        }
    }
    subscript(s:String)->JSON {
        switch self {
        case let .JSObject(d):
            return d[s]!
        default:
            return .JSNull
        }
    }
}

ここまでくれば、

json2["array"][2].asNumber! - 0.195 == 42 // true

という具合にJavaScriptのJSONにさほどひけをとらない使い心地になっています。さらに==を定義してEquatableプロトコルに準拠したり、forループに直接かけられるようにしたりしていけば、立派なJSONモジュールができあがることでしょう。

次号予告

ところで前回はもう1つ謎がありました。前回作ったNestedArrayはなぜ、IntだけではなくDoubleStringでもいけるのでしょう。次回はそれを可能にしている総称型(generics)に焦点をあてます。

Software Design

本誌最新号をチェック!
Software Design 2022年9月号

2022年8月18日発売
B5判/192ページ
定価1,342円
(本体1,220円+税10%)

  • 第1特集
    MySQL アプリ開発者の必修5科目
    不意なトラブルに困らないためのRDB基礎知識
  • 第2特集
    「知りたい」「使いたい」「発信したい」をかなえる
    OSSソースコードリーディングのススメ
  • 特別企画
    企業のシステムを支えるOSとエコシステムの全貌
    [特別企画]Red Hat Enterprise Linux 9最新ガイド
  • 短期連載
    今さら聞けないSSH
    [前編]リモートログインとコマンドの実行
  • 短期連載
    MySQLで学ぶ文字コード
    [最終回]文字コードのハマりどころTips集
  • 短期連載
    新生「Ansible」徹底解説
    [4]Playbookの実行環境(基礎編)

おすすめ記事

記事・ニュース一覧