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の実行環境(基礎編)