Swift on FreeBSD
本題に入る前にニュースを1つ。実はFreeBSDでもSwiftは動きます。ports
しかしビルドエラーの内容を精査してみると、問題はSwiftのソースコードではなく、ビルドが依存しているツールがアップデートされているのにそれにあわせてportsが更新されていないのが原因と判明したので、その点を微修正してビルドした後パッケージ化したものを公開しました。
同パッケージはもともとevalparkのためにビルドしたものです。evalparkというのは任意のシェルスクリプトをフルセットのFreeBSD 11で実行してJSONで返すというすごーいWeb APIで、これだけでまるまる別記事を書けてしまうのですが、それはともかく同サービスを使うとWeb上でSwift on FreeBSDを実行できます
 
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.
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.ではなく`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というJSONSerialization.の戻り値は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.を返したいから。
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.を使えば文字列からも初期化できます。やってみましょう。
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と文字列の相互変換はバッチリです。が、まだまだ使いづらい。値も取り出せるようにしましょう
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だけではなくDoubleやStringでもいけるのでしょう。次回はそれを可能にしている総称型
本誌最新号をチェック!
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の実行環境(基礎編)

