From AP to Z
本題に入る前に臨時ニュースを。本連載第18回 でも紹介したAPFSですが、iOS 10.3より正式導入が始まるようです 。本記事執筆現在、iOS 10.3はまだβですが、OSアップデートすると同時にストレージがHFSXからAPFSにアップデートされます。本記事がみなさんのもとに届くころにはiOS 10.3とともにAPFSが我々のiPhoneやiPadに届いているかもしれません。
それよりずっと地味ながらも見落とせないのは、ZFSのmacOS向けオープンソース実装であるOpenZFS on OS X のSierra正式対応(図1 ) 。FreeBSDやLinuxといったほかのOpenZFS対応OSで作成されたストレージをMacから読み書きできるにとどまらず、なんと同じZFSプールから対応OSをマルチブートできてしまいます(図2 ) 。
図1 OpenZFS on OS X
図2 ZFSプールから対応OSをマルチブート
すでにインストーラまでZFSに対応しているFreeBSD以外のOSのZFSブート環境構築はまだまだ面倒なのですが、これだけ毛色の異なるOSでファイルシステムがこのレベルで共通して使えることに、オープンソースというものの威力をあらためて実感しています。
オープンソースといえば、Swiftもそうでした。Swiftの父Chris Lattner のApple退社はSwift界隈で当然話題になりましたが、FreeBSDProjectの父Jordan Hubbard もAppleに入社しその後退社しています。「 父なき」あとも、両プロジェクトとも繁栄していることを鑑みれば、オープンソース化というのはプロジェクトのサバイヴァビリティ向上を大いに改善するのは確かなようです。あのMicrosoftですら、NadellaのCEO就任後はオープンソースへの態度を一変しているのも宜(むべ)なるかな。
型の集まりもまた型
ニュースはこれくらいにして本題に入りましょう。前回 に引き続き今回も型の話です。今回は配列(array)や辞書(dictionary)のような、複数の値をまとめて扱うための「まとめ型」について、Arrayを例にとりながら学びます。
Swiftの型は静的。当然配列や辞書も静的な型を持つのですが、動的な型を持つ言語の配列や辞書に慣れていると、面食らうことが1つあります。
まずは、RubyとJavaScriptの配列を見てみましょう。
Rubyの例
% irb
irb(main):001:0> [nil,false,0,"",[],{}].each{¦e¦ p e.class}
NilClass
FalseClass
Fixnum
String
Array
Hash
=> [nil, false, 0, "", [], {}]
JavaScriptの例
% node
> [null,false,0,"",[],{}].forEach(function(e){console.log(typeof e)})
object
boolean
number
string
object
object
undefined
>
見てのとおり、配列型には異なる型の要素をなんでも入れることができます。なので配列自体の型は1種類で間に合います。
Swiftはどうでしょう?
図3 のように文句を言われてしまいました。もう少し見ていきましょう。
図3 Swiftでの配列実験の結果
% swift
Welcome to Apple Swift version 3.0.2 (swiftlang-800.0.63 clang-800.0.42.1). Type :help for assistance.
1> [nil,false,0,"",[],[:]]
error: repl.swift:1:1: error: type of expression is ambiguous without more context
[nil,false,0,"",[],[:]]
1> [false,true]
$R0: [Bool] = 2 values {
[0] = false
[1] = true
}
2> [0,1]
$R1: [Int] = 2 values {
[0] = 0
[1] = 1
}
3> ["","one"]
$R2: [String] = 2 values {
[0] = ""
[1] = "one"
}
そう。Swiftでは配列の要素の型はすべて同じで、そしてElementという要素の型を持つ配列の型は[Element]
という型になるのです。
なんて型苦しい? ところでこのRubyコードを見てくれ。図4 をどう思う?
図4 Rubyの実行結果
% irb
irb(main):001:0> ["answer",42,42.195,[true],false].each{¦e¦ p e + e}
"answeranswer"
84
84.39
[true, true]
NoMethodError: undefined method `+' for false:FalseClass
from (irb):2:in `block in irb_binding'
from (irb):2:in `each'
from (irb):2
from /usr/bin/irb:12:in `<main>'
すごく……大きなバグを誘発しそうな気がしませんか?
こういう場合、動的言語ではほとんどの場合、実行時に都度不正な値が入っていないかをチェックする、いわゆるvalidationで解決しています。たとえばこんな感じ。
% irb
irb(main):001:0> ["answer",42,42.195,[true],false].each do ¦e¦
irb(main):002:1* p e + e if e.respond_to?(:+)
irb(main):003:1> end
"answeranswer"
84
84.39
[true, true]
=> ["answer", 42, 42.195, [true], false]
しかしそれはその分コードの量が増えるということでもあり、増えたコードの分だけバグの可能性も増えるということでもあります。配列の要素の型が一定であれば、そうした心配はずっと減ります。
1> [41,42].map{$0+$0}
$R0: [Int] = 2 values {
[0] = 82
[1] = 84
}
2> [41.0,42.195].map{$0+$0}
$R1: [Double] = 2 values {
[0] = 82
[1] = 84.39
}
3> ["answer","universe"].map{$0+$0}
$R2: [String] = 2 values {
[0] = "answeranswer"
[1] = "universeuniverse"
}
4> [false,true].map{$0+$0}
error: repl.swift:4:20: error: binary operator'+' cannot be applied to two 'Bool' operands
[false,true].map {$0+$0}
型がないなら作ればいいのに
しかしそれではJSONのような入れ子があるデータはどうしたらよいのでしょう? 答えは単純。そのような型を作ってしまえばよいのです。たとえば[Int]
を入れ子にしたいとしたら、
enum NestedArray<Element> {
case V(Element)
case A([NestedArray])
}
という型を宣言しておいてから、
var na = NestedArray.A([
NestedArray.V(0),
NestedArray.V(1),
NestedArray.A([
NestedArray.V(2),
NestedArray.V(3)
])
])
と書けばおk……って何これ長い!
もちろん、もっと短くも書けます。
var na:NestedArray = .A([
.V(0), .V(1), .A([.V(2), .V(3)])
])
あらかじめ型を宣言しておけば、その中の要素の種類は省略してこのように書けてしまいます。しかし中の値にどうやってアクセスすれば?
ここから本格的な型作りが始まります。
まずは手始めに中の値にアクセスできるようにしてみましょう。
extension NestedArray {
var value:Element? {
switch self {
case let .V(v):
return v
default:
return nil
}
}
var array:[NestedArray]? {
switch self {
case let .A(a):
return a
default:
return nil
}
}
}
これを実行すると次のようになります。
na.array?[0].value // 0
na.array?[1].value // 1
na.array?[2].value // nil
na.array?[2].array?[0].value // 2
どうやらうまくいったようです。
でも、本来配列なのに.array
で中身を取り出さなければならないなんておかしいですよね?
添字で直接中身を読み書きできない?
extension NestedArray {
subscript(i:Int)->NestedArray? {
get {
switch self {
case let .A(a):
return a[i]
default:
return nil
}
}
set {
switch self {
case var .A(a):
a[i] = newValue!
self = .A(a)
default:
return
}
}
}
}
これも実行すると、次のような結果が表示されます。
na[0]?.value // 0
na[1]?.value // 1
na[2]?.value // nil
na[2]?[0]?.value // 2
na[0] = .V(42)
na[0]?.value // 42
na[1]?.value // 1
直接.map()
できない? そのままfor
ループにかけられない? いっそきちんとしたSequence
になるまで発展させられない?
―あとは読者の宿題ということで。
このような感じで必要な機能をextension
に追加していくのが、Swiftにおける「型作り」になります。
次号予告
ところで今回作ったNestedArray
は、Int
だけではなく、Double
はString
でもいけます。
var nad:NestedArray = .A([
.V(0.0), .V(1.1), .A([.V(2.2), .V(3.3)])
])
var nas:NestedArray = .A([
.V("zero"), .V("one"), .A([.V("two"), .V("three")])
])
とくにそう宣言していないのになぜそれができたのでしょうか?
ところでSwift
にはAny
というなんでも入る型がすでに存在しています。
var a:Any = [0,1,[2,3]]
結論から先に言うと、Any
は避けるべきです。少なくとも、動的言語の変数のように使うべきでありません。しかし、その理由はいったいなぜなのか?
次回はこれらの疑問に答えつつ、Swiftの型の核心に迫っていきます。
第1特集
MySQL アプリ開発者の必修5科目
不意なトラブルに困らないためのRDB基礎知識
第2特集
「知りたい」「 使いたい」「 発信したい」をかなえる
OSSソースコードリーディングのススメ
特別企画
企業のシステムを支えるOSとエコシステムの全貌
[特別企画]Red Hat Enterprise Linux 9最新ガイド
短期連載
今さら聞けないSSH
[前編]リモートログインとコマンドの実行
短期連載
MySQLで学ぶ文字コード
[最終回]文字コードのハマりどころTips集
短期連載
新生「Ansible」徹底解説
[4]Playbookの実行環境(基礎編)