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
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
だけではなくDouble
やString
でもいけるのでしょう。次回はそれを可能にしている総称型(generics)に焦点をあてます。
第1特集
MySQL アプリ開発者の必修5科目
不意なトラブルに困らないためのRDB基礎知識
第2特集
「知りたい」「使いたい」「発信したい」をかなえる
OSSソースコードリーディングのススメ
特別企画
企業のシステムを支えるOSとエコシステムの全貌
[特別企画]Red Hat Enterprise Linux 9最新ガイド
短期連載
今さら聞けないSSH
[前編]リモートログインとコマンドの実行
短期連載
MySQLで学ぶ文字コード
[最終回]文字コードのハマりどころTips集
短期連載
新生「Ansible」徹底解説
[4]Playbookの実行環境(基礎編)