Swiftにおけるダック・タイピングの導入法

みなさんはダック・タイピングという言葉を聞いたことがあるでしょうか。ダック・タイピングの取り入れ方を、Swiftを例にご紹介します。

Swiftでダック・タイピングを書くメリット

ダック・タイピングとは

プログラミングで行う型付け作法のことです。名前の由来は、「アヒルのように歩き、アヒルのように鳴くものはアヒルに違いない」という言い回しからきています。

例えば、Go言語では interface を使うことで扱う型の制限が可能ですが、もしinterface 定義の際に宣言したメソッドをすべて満たす struct を作れば、この structinterface を実装していなくても、実装しているように見せることができます。

C++の template は、ダック・タイピングの静的版です。俗にいう「同じインターフェースをもつ」とは、コンパイラにとってインターフェースが同じであり、実際の中身は関係ありません。

ダック・タイピングを書くメリット

プログラミングをしていると、クラス内や関数内で実装している型の詳細を隠したい場面があると思います。もしくは、静的型付けの使いすぎを防ぎたいときや、異なる型を相互運用したいときなど、そんな時に便利なのがダック・タイピングです。

プロトコルや、abstructとスーパークラスなどもダック・タイピングの一部と言えます。

例としてNSStringを見てみると、NSStringは常にサブクラスのインスタンスで、通常は private として扱われます。つまり、ユーザーやAPIから中身の型を見られることはありません。

Swiftでは、より上級なテクニックを使うことでプロトコルや一般的な型を便利に扱うことができます。たとえばInt 型のシークエンスを受け付けるコードを書きたくても、以下のようには書けません。

func f(seq: Sequence<Int>) { …

一般的な型を指定することはできますがプロトコルまではできません。そこでコードを少し変えます。

func f<S: Sequence>(Seq: S) where S>Element == Int { …

これで機能する場合もありますが、うまくいかない場合もあります。一般型を使うときは、一箇所にのみ足すことはできないので他の値も一般型にする必要があります。また、このコードでは返り値を得ることができません。

func g<S: Sequence>() -> S where S.Element == Int { …

このコードだと、 g に返り値を求めることができますが、入力値を適切な型にする必要があります。

Swift ではこれを解決する型、AnySequence があります。AnySequence 型は抽象的なシークエンスをラッピング(包み込む)することで、シークエンスの型に関係なくデータを扱うことができます。

func f(seq: AnySequence<Int>) { …
 func g() -> AnySequence<Int> { …

このコードでは、一般型が消え特定の型は見えないようになっています。AnySequence内の値をラッピングする為に実行時間が少し長くなりますが、とても簡潔なコードです。

Swift の標準ライブラリにはこのような型(AnyCollection、 AnyHashable、 AnyIndex)がたくさんあります。ダック・タイピングは、独自の一般型やプロトコルを作りたい時や、コードをよりシンプルにしたい時に役立ちます。

ここからは、ダック・タイピングの他の方法を紹介します。

クラスなしで書くダック・タイピング

複数の関数と型を、型の種類を晒すことなくラッピングするとします。どこかスーパークラスとサブクラスの関係に似ている気がしませんか?実は、実際にサブクラスを使ってダック・タイピングを行うことができます。スーパークラスのみをAPIから見えるようにすることで、その下にある実装型を晒すことなく、サブクラスに型を実装できます。

早速このテクニックを紹介したいと思います。ここではクラス名を MAnySequence とします。

class MAnySequence<Element>: Sequence {

makeIteratorメソッドの返り値を受け取るためのイテレータを付け足しましょう。なぜかというと、ここでは Sequence の型とイテレータの型を隠す為に、ダック・タイピングを二度行う必要があるからです。また、このイテレータクラスを IteratorProtocol と名付け、メソッドに fatalError を含む next を設けます。Swift には抽象クラスがないのでこのコードで十分です。

   class Iterator: IteratorProtocol {
       func next( ) -> Element? {
          fatalError("Must override next( )")
       }
    }

MAnySequence は makeIterator と似たような実装をしています。fatalError を呼びサブクラスが上書きできるようにします。

   func makeIterator( ) {
       fatalError( "Must override makeIterator( )" )
    }
 }

これがダック・タイピングで書いたpublic APIです。private の実装でサブクラス化しています。そしてpublicクラスをエレメントの型でパラメータ化し、privateの実装クラスは ラッピングされたシークエンスの型でパラメータ化しています。

private class MAnySequenceImpl<Seq: Sequence>: MAnySequence<Seq.Element> {

Internalイテレータに対応するinternalサブクラスをクラス内で定義する必要があります。

   class IteratorImpl: Iterator {

シークエンスIterator型のインスタンスをラッピングします。

      var wrapped: Seq.Iterator

      init(_ wrapped: Seq.Iterator) {
          self.wrapped = wrapped
       }

ラッピングされたイテレータを呼ぶための next を返します。

      override func next( ) -> Seq.Element? {
          return wrapped.next( )
       }
    }

同じように、シークエンスのインスタンスをMAnySequenceImplがラッピングします。

   var seq: Seq

   init(_ seq: Seq) {
       self.seq = seq
    }

ラッピングされたシークエンスからイテレータを得る為に、makeIteratorを実装します。そして得たイテレータを IteratorImpl: 内でラッピングします。

      override func makeIterator( ) -> IteratorImpl {
          return IteratorImpl( seq.makeIterator( ) )
       }
 }

それではこれらを実際に作って行きましょう。MAnySequenceのstaticメソッドがMAnySequenceImplのインスタンスを作り出し、MAnySequenceとして返します。

extension MAnySequence {
    static func make<Seq: Sequence>(_ seq: Seq) -> MAnySequence<Element> where Seq.Element == Element{
       return MAnySequenceImpl<Seq>(seq)
    }
 }

プロダクションコードでは、もう一段階のレベルを設けて、MAnySequnceを初期化できるようにするといいでしょう。

それでは実行してみましょう!

func printInts(_ seq: MAnySequence<Int>) {
    for elt in seq {
       print(elt)
    }
 }

let array = [1, 2, 3, 4, 5]
 printInts(MAnySequence.make(array) )
 printInts(MAnySequence.make(array[1 ..< 4]) )

しっかり動作したでしょうか?

関数を使わないままで書くダック・タイピング

次は、入力する型を公開せずに関数を実行する方法です。流れとしては、関数のシグネチャに含まれる型を、公開しても良いものだけ使うようにします。すると、実装されている型をわかった上で関数をデザインすることができるようになります。

MAnySequenceを使った場合どのようになるのか見てみましょう。先ほどの実装と似ていますが、classの代わりにstructを使っています。

struct MAnySequence<Element>: Sequence {

戻り値のIteratorを付け足して、関数を含むstructを作ります。この関数のパラメータはありませんが、戻り値としてElement? を返します。そしてこれを、IteratorProtocolの中にあるnextメソッドのシグネチャとして使います。その実装がこうなります。

   struct Iterator: IteratorProtocol {
       let _next: () -> Element?
 
       func next() -> Element? {
          return _next()
       }
    }

MAnySequence自体も似ていて、引数を取らずにIteratorを返す関数を定義しています。Sequenceはこの関数を通して実装されます。

   let _makeIterator: () -> Iterator
 
    func makeIterator() -> Iterator {
       return _makeIterator()
    }

MAnySequence内のinitがこのダック・タイピングの鍵となります。抽象型のSequenceをパラメータとします。

   init<Seq: Sequence>(_ seq: Seq) where Seq.Element == Element {

シークエンスの機能を関数の中にラッピングします。

      _makeIterator = {

seqを使ってイテレータを作ります。

         var iterator = seq.makeIterator()

そしてこのイテレータをIterator.でラッピングします。また、_next関数がiteratorのnextメソッドをコールします。

         return Iterator(_next: { iterator.next() })
       }
    }
 }

実際のサンプルコードがこちらです。

func printInts(_ seq: MAnySequence<Int>) {
    for elt in seq {
       print(elt)
    }
 }

let array = [1, 2, 3, 4, 5]
 printInts(MAnySequence(array))
 printInts(MAnySequence(array[1 ..< 4]))

動作したでしょうか?

この関数ベースのダック・タイピングは、小さい関数を大きな型内にラッピングしたいときに特に便利です。別のクラスを作って関数を実装する必要が無くなります。

例えば、様々な型を扱えるコードを書くとしましょう。しかし本当に必要な機能は、ゼロベースでカウントすることだけだとします。テーブル表示されたデータを扱うと仮定すると、このようなコードを書くことができます。

class GenericDataSource<Element> {
    let count: () -> Int
    let getElement: (Int) -> Element
 
    init<C: Collection>(_ c: C) where C.Element == Element, C.Index == Int {
       count = { c.count }
       getElement = { c[$0 - c.startIndex] }
    }
 }

GenericDataSource内では、count()とgetElement()をコールすることで、いつでも関数を実行することができます。また、パラメータの型もGenericDataSourceの一般型に合わせる必要がありません。

ダック・タイピングをマスターしよう

ダック・タイピングは、コード内に多く存在しがちな一般型を減らすのにとても役立ちます。抽象型のpublicスーパークラスと、privateのサブクラスを使ったり、APIを関数内にラッピングすることで、インターフェイスをシンプルに保つことができます。

関数を使わないダック・タイピングは、ほんの少しの機能だけ必要な場合に特に役に立ちます。

Swiftの標準ライブラリにもダック・タイピングの機能が備わっています。たとえば、AnySequenceはSequenceをラッピングして、型の種類を知ることなくそのシークエンスをイテレータすることができます。AnyIteratorも似たようなもので、ダック・タイピングのイテレータを行うことができます。AnyHashableはHashable型のダック・タイピングを可能にします。

他にもいくつかありますが、これらはAnyの項目を調べると見ることができます。標準ライブラリではダック・タイピングをCodable APIの一環として扱っています。KeyedEncodingContainerとKeyedDecodingContainerはコンテナプロトコルに対応したダック・タイピングのラッピングで、EncoderとDecoderをコンテナの型に関係なく扱うことができるようになります。

一度マスターするととても便利なダック・タイピングなので、興味を持たれた方はぜひ使ってみることをおすすめします。

(翻訳:Juri Ando)

SHARE

RELATED

  • お問い合わせ
  • お問い合わせ
  • お問い合わせ