書いて覚えるSwift入門

第24回型の探求

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
図1 OpenZFS on OS X
図2 ZFSプールから対応OSをマルチブート
図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だけではなく、DoubleStringでもいけます。

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の型の核心に迫っていきます。

Software Design

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

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

  • 第1特集
    設計・開発のイメージが湧く!
    Web APIの作り方
  • 第2特集
    WebエンジニアのためのDNS速習講座
    名前解決のしくみを説明できますか?
  • 特別企画
    「Interop Tokyo 2022」現地レポート
    進化を続けるインターネット技術の最前線をのぞく
  • 特別企画
    MySQL×機械学習 HeatWave MLが変えるデータ活用のかたち
    [後編]HeatWave MLで機械学習のモデル作成・予測・検証を行う
  • 短期連載
    MySQLで学ぶ文字コード
    [2]COLLATIONを正しく理解する
  • 短期連載
    新生「Ansible」徹底解説
    [3]Ansibleの使い方

おすすめ記事

記事・ニュース一覧