書いて覚えるSwift入門

第12回Protocol Oriented Programming

すでに始まったPOP
(Protocol Oriented Programming)の時代

今回はいよいよ懸案のProtocol Oriented Programmingについて解説します。

Swift Standard Library[1]を眺めていると、際立った特徴があります。

Classが少ないのです。たったの5つ。しかも1つは継承でつながっているので実質3つ図1⁠。

図1 Swift Standard Library
図1 Swift Standard Library

それに対してStructとEnumとProtocolはどっさりあります。これは何を意味するのか?

SwiftにおいてClassというのはあくまでもObjective-Cの遺産を活用するためのものであって、Swift的なProgramとはStructやEnumをProtocolで「つなげて」活用することであるという「中の人の心の叫び」であると弾言しておきます。

ClassとStructやEnumの違い

それを理解するためには、ClassとStructやEnumの違いを理解しておく必要があります。細かい違いは数あれど、⁠若者のClass離れ」の理由を理解するために必要なのは、ただ1つです。

Classは継承できるが、StructやEnumは継承できない。つまり、リスト1の事例のようなコードは書けないのです。

リスト1 Class継承のサンプル
class ClassV1 {
    var x = 0.0
    init (x:Double) {
        self.x = x
    }
}
class ClassV2 : ClassV1 {
    var y = 0.0
    init (x:Double, y:Double) {
        super.init(x:x)
        self.y = y
    }
}
class ClassV3 : ClassV2 {
    var z = 0.0
    init (x:Double, y:Double, z:Double) {
        super.init(x:x, y:y)
        self.z = z
    }
}
var cv3 = ClassV3(x:1, y:2, z:3)

なぜ継承できないのか? 参照型であるClassと異なり、StructやEnumには実体があるからです。上記の例ではインスタンス変数は実際3つあり、それが参照でつながっています。xにアクセスするには親クラスの親クラスまで参照をたぐらなければなりません。sizeofValue(cv3)はポインターのサイズである8。Classのインスタンスであれば、必ずそうなります。

これに対し、StructやEnumは、必要なインスタンスはすべて自前で持っています。

struct StructV3 {
    var x = 0.0, y = 0.0, z = 0.0
    init (x:Double, y:Double, z:Double) {
        self.x = x
        self.y = y
        self.z = z
    }
}
let sv3 = StructV3(x:1, y:2, z:3)

ここでsizeofValue(sv3)は、Doubleのきっかり3倍である24。確かに実体を持っています。StructやEnumは、何も共有していないのです。Shared Nothingというは、並列プログラミングを格段に容易にします。共有は競合を産み、その競合をどう調停するかで我々はかなり苦労してきました。なら共有しなければいい。簡単ですね?

「えー、ちょっと待って! それってDRY(Don't repeat yourself)に反しない?」と反応した読者は鋭い。そうなのです。共有をやめるということは、型の数だけ実装がいるということでもあるのです。共通項を取り出してまとめるというのは、プログラミングの作法で最重要なものの1つなのに。

でも、それはソースのレベルの話であって、必要なマシンコードはコンパイラーのほうで生成してくればいいじゃん?――我々がしたいのは、ソースの共有であって、実体ではないのですから。

それを可能にするのが、Protocolなのです。

実践例[swift-complex]

論より証拠。実例を見てみましょう。[swiftcomplex]というgithub projectがあります。Swiftの演習用に筆者がずっと書いてきたものですが、今回の記事のためにごっそり書き直しました図2⁠。

図2 swift-complex
図2 swift-complex

要は複素数を使うためのライブラリです。使い心地はRubyであれば、

require 'cmath'
include cmath

Pythonであれば

from cmath import *

したときにとてもよく似ています。余談ではありますが、去年のクリスマスにリリースされたPerl 6では、複素数サポートは組み込みです。

そのまま遊べるように、Playgroundも用意してあります。みんな大好きマルデンブロー集合もこのとおり図3⁠。

図3 みんな大好きマンデルブロー集合
図3 みんな大好きマンデルブロー集合

で、自分で言うのもなんですが、割によく書けていると思います。よく書けているというのは、

Swiftの特徴を活かしていること
  • 演算子関数による直感的な操作
    →Playground
  • クロスプラットフォーム
    →OS XやiOSだけでなく、Linuxでも動く
「実数」の実装が入れ替え可能であること
  • 現時点でDoubleだけでなく、FloatやIntもサポート
  • 任意制度の数値ライブラリを別途用意すれば、それを使うことも可能

では実際に見てみましょう。500行ちょっとしかないので全部掲載したいところですが、紙幅が足りないので要点だけリスト2⁠。

リスト2 複素数実装のサンプル
public protocol ArithmeticType:
AbsoluteValuable, Equatable, カ
Comparable, Hashable {
    // Initializers (predefined)
    init(_: Int)
  //// [中略]
  init(_: Double)
    init(_: Float)
    init(_: Self)
    // CGFloat if !os(Linux)
    #if !os(Linux)
    init(_: CGFloat)
    #endif
    // Operators (predefined)
    prefix func + (_: Self)->Self
    prefix func - (_: Self)->Self
    func + (_: Self, _: Self)->Self
    func - (_: Self, _: Self)->Self
    func * (_: Self, _: Self)->Self
    func / (_: Self, _: Self)->Self
    func += (inout _: Self, _: Self)
    func -= (inout _: Self, _: Self)
    func *= (inout _: Self, _: Self)
    func /= (inout _: Self, _: Self)
}

まず、Protocolで複素数の要素として最低限満たしておくべき要件を列挙しておきます。

  • Swiftの組込み型からの初期化をサポートしていること
  • 基本的な四則演算をサポートしていること

というのをSwift語で書き下しただけです。

で、Intはすでにこれらを満たしているので、

extension Int : ArithmeticType {}

とすでにArithmeticTypeに準拠(conform)していますよ、と一言で済みます。

ここまではSwift 1の時代からあったのですが、Swift 2の時代ですごいのは、ここですリスト3⁠。

リスト3 ArithmeticTypeの実装サンプル
public extension ArithmeticType {
    /// self * 1.0i
    public var i:Complex<Self> カ
{ return Complex(Self(0), self) }
    /// abs(z)
    public static func abs(x:Self)->Self { カ
return Swift.abs(x) }
    /// failable initializer to conver the type
    /// - parameter x: `U:ArithmeticType` カ
where U might not be T
    /// - returns: Self(x)
    public init?<U:ArithmeticType>(_ x:U) {
        switch x {
        case let s as Self: self.init(s)
        case let d as Double: self.init(d)
        case let f as Float: self.init(f)
        case let i as Int: self.init(i)
        default:
            return nil
        }
    }
}

ご覧のとおり、42.iとかと書くと(0+42.i)になるのは、ここでやっています。わざわざ他の型で実装する必要はないんです。

Protocolといういうのは、あくまで規約であって、その規約をどう満たすかはそのProtocolを準拠する型(types)に任されてきたのですが、Swift 2になって、Protocol Extensionで実装まで一緒にできるようになったのです。つまり、準拠する型が10あれば10、100あれば100、一挙にメソッドやプロパティを追加することが可能になったのです。これはすごい。

実際Swift 2では、ArrayだけではなくSequenceに準拠する型であればすべて.map.reduceが使えるようになったのですが、まさにProtocol Extensionの賜物と言えるでしょう。

ではいよいよComplexを見てみましょうリスト4⁠。

リスト4 Complexの実装
public struct Complex<T:ArithmeticType> : Equatable, CustomStringConvertible, Hashable {
    public typealias Element = T
    public var (re, im): (T, T)
  //// [中略]
}

リスト4は見てのとおり、ArithmeticTypeに準拠したTによる総称型です。1+1.iComplex<Int>1.0+1.0.iComplex<Double>になるわけです。

ところで賢明な読者は、この時点で絶対値.absや偏角.argがないことに気づかれるかもしれません。これらは複素数自体がComplex<Int>つまりガウス整数であっても整数におさまるとは限らないからです。

うまいこと、整数の場合は整数を返さないメソッドを持たせず、しかし「実数」の場合にはこれを追加するということができるのでしょうか? できます。そう。Swiftならね。

まず、ArithmeticTypeの要件をすべて満たす上位互換Protocolを1つ追加しますリスト5⁠。

リスト5 上位互換Protocolの追加
public protocol RealType : ArithmeticType, FloatingPointType {
    static var EPSILON:Self { get } // for =̃
}

そしてこれをProtocol Extensionで拡張しますリスト6⁠。

リスト6 Protocol Extensionによる拡張
extension RealType {
    /// Default type to store RealType
    public typealias Real = Double
    //typealias PKG = Foundation
    // math functions - needs extension for each struct
    #if os(Linux)
    public static func cos(x:Self)-> Self { return Self(Glibc.cos(Real(x)!))! }
  //// [中略]
    #else
    public static func cos(x:Self)-> Self { return Self(Foundation.cos(Real(x)!))! }
  //// [中略]
    #endif
}

要するに、三角関数や指数関数などをLinuxであればGlibcそうでなければFoundationからごっそり持ってくるわけです。ちなみにProtocol Extensionと型のExtensionで同名の識別子がある場合、型のほうが優先して使われます。実際[swift-complex]でも、Floatに関してはいったんDoubleに変換してFloatに戻すのではなくFloatのままで計算するためにcosfなど末尾にfがついた関数を使いたかったので、extension Floatで上書きしています。

そうしたうえで、リスト7です。

リスト7 Swift 2ならではの実装例
extension Complex where T:RealType {
    public init(abs:T, arg:T) {
        self.re = abs * T.cos(arg)
        self.im = abs * T.sin(arg)
    }
    /// absolute value of self in T:RealType
    public var abs:T {
        get { return T.hypot(re, im) }
        set(r){ let f = r / abs; re *= f; im *= f }
    }
    /// argument of self in T:RealType
    public var arg:T {
        get { return T.atan2(im, re) }
        set(t){ let m = abs; re = m * T.cos(t); im = m * T.sin(t) }
    }
    /// projection of self in Complex
    public var proj:Complex {
        if re.isFinite && im.isFinite {
            return self
        } else {
            return Complex(
                T(1)/T(0), im.isSignMinus ? -T(0) : T(0)
            )
        }
    }
}

つまり複素数の要素がRealTypeに準拠してある場合にのみ、.abs.argを追加するということがSwift 2で可能になったのです。

あとは、関数や演算子を粛々と定義していけばいいだけです。たとえば除算/はこんな感じ。

public func / <T>(lhs:Complex<T>,
rhs:Complex<T>) -> Complex<T> {
    return (lhs * rhs.conj) / rhs.norm
}

共役.conjとノルム.normは、Complex<T>であれば必ず持っているので、複素数の掛け算と複素数と実数の掛け算でこのように定義できるわけです。実際のソースをGitHubでご覧いただくと、ほとんどすべての演算子がこのような1行定義になっています。

次に"cmath⁠な関数を見てみましょう。

public func exp<T:RealType>(z:Complex<T>)
-> Complex<T> {
    let r = T.exp(z.re)
    let a = z.im
    return Complex(r * T.cos(a), r *
T.sin(a))
}

「博士の愛した数式」(小川洋子)でもお馴染みの、e ** (x+y.i) = e**r * (cos(y) + i*sin(y))そのままですね。ただしcosでなくてT.cosと書いています。RealTypeのProtocolでpublic static func cos(x:Self)->Selfとなっているものを指定しています。なぜメソッドではなく型関数(static method)かというと、既存の型をなるべく上書きしたくなかったから。かつてはメソッドとして追加していたのですが、その方法だとXcodeなどで.cosまで補完されてしまってちょっと驚きなのです。Rubyistsなどからするとちょっと残念かもしれませんが。

Todo

というわけで弾言します。総称関数とプロトコルを制するものが、Swiftを制するのだ、と。Swift 2のprotocol extensionで、その可能性はさらに高まりました。

とはいえ、Swift 2でもまだ至らないことも多々あります。たとえばプロトコルに準拠するためのメソッドや関数を書いている最中には、⁠type Foo does not conform to protocol Bar⁠というエラーメッセージでXcodeが真っ赤になったりするのですが、具体的にどんなメソッドやプロパティが足りないかを一挙に調べてくれるとうれしいのですが。

あと、LinuxでもimportFoundationできるのに、これがOSX/iOSのそれと全然違うってのも悩ましい。なるべく#ifを書かずに済ませたいのに……。

それにしても、これほど書いていて楽しい言語というのはそうありません。IBM Swift Sandboxのおかげでブラウザからも試せるようになったSwift、皆さんもぜひ遊んでみてください。

Software Design

本誌最新号をチェック!
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の実行環境(基礎編)

おすすめ記事

記事・ニュース一覧