前回へ LINQ 記事一覧へ

Swift 2.1 の SequenceType への LINQ ライクな拡張

2015-12-17 (鈴)

1. はじめに

前回「Swift 2.0 による LINQ to Objects の試作」で与えた Swift に対する LINQ to Object (以下,単に LINQ) の実装は,その第4節で簡単に議論したように標準ライブラリに含まれる構造体 LazySequence (より正確にはそのプロトコル LazySequenceType) と Select, Where メソッドの機能が重複していた。

そこで,今回は LazySequenceType の存在を前提として Swift 2.1 の SequenceType の自然な拡張となるように小改訂した LINQ 実装を与える。 Swift 処理系として OS X 10.10.5 上の Xcode 7.2 に付属する Swift 2.1.1 を使う。

以下,前回の記述順序に準じて今回の実装を説明する。

2. Linq モジュールの構成

Linq モジュールを linq.swift ファイルで構成する。 linq.swift ファイルは抽象的な列を表す標準プロトコル SequenceType を下記のように拡張する。

public extension SequenceType {

    private typealias E = Generator.Element

    ....
}

SequenceType に随伴する Generator 型の要素型 Generator.Element が列の要素型となる。 簡単のため typealias を使って E の1語で要素型を参照できるようにする。

ターミナル上で Linq モジュールを作成するときは swiftc コマンドに下記のように -emit-library と -emit-module オプションを与えてモジュール・ファイルと共有ライブラリを作らせればよい。 -O オプションは C コンパイラと同じく最適化を指示する。 -module-name オプションで構築するモジュールの名前 (ここでは Linq) を指定する。 -sdk オプションでコンパイル対象の SDK のパスを与える。

01:~/tmp$ swiftc linq.swift -O -module-name Linq -sdk `xcrun --show-sdk-path` \
> -emit-library -emit-module
01:~/tmp$ ls -l *Linq*
-rw-r--r--  1 suzuki  staff   2292 Dec 16 12:36 Linq.swiftdoc
-rw-r--r--  1 suzuki  staff  11772 Dec 16 12:36 Linq.swiftmodule
-rwxr-xr-x  1 suzuki  staff  28760 Dec 16 12:36 libLinq.dylib*
01:~/tmp$  

swift に対し -I オプションでモジュール・ファイルに,-L オプションで共有ライブラリにそれぞれパスを通し,-lLinq で libLinq ライブラリをリンクして起動する。

01:~/tmp$ swift -I. -L. -lLinq
Welcome to Apple Swift version 2.1.1 (swiftlang-700.1.101.15 clang-700.1.81). Ty
pe :help for assistance.
  1>  

このとき swift から Linq モジュールを import して LINQ メソッドを実行できる。

  1> import Linq
  2>  

以下,linq.swift の各 LINQ メソッドの実装を説明する。

3. LINQ メソッドの実装

3.1 toArray メソッド

Array 型は init 引数として SequenceType を取ることができる。 これを使えば LINQ の ToArray メソッドはごく簡単に実装できる。 次のように SequenceType を拡張すればよい。

    /// 列の各要素からなる配列を新しく作って返す。
    func toArray() -> Array<E> {
        return Array(self)
    }

Swift の命名法にしたがってメソッド名を大文字ではなく小文字で始める。 以下,3.2 節以降のメソッドも同様である。

SequenceType に適合する任意の型で toArray() を使うことができる。

  2> (1...10).toArray()
$R0: [Int] = 10 values {
  [0] = 1
  [1] = 2
  [2] = 3
  [3] = 4
  [4] = 5
  [5] = 6
  [6] = 7
  [7] = 8
  [8] = 9
  [9] = 10
}
  3>  
LINQ の Select メソッドは LazySequenceType の map メソッドで代用できます。 平たく言えば map の代わりに lazy.map を使えば OK です。 例を示します。
  3> for i in [7, 8, 9, 2].lazy.map({$0 * 100}) { print(i) }
700
800
900
200
  4>  
どこがどう遅延しているのかよく分かりませんから print をさしはさんでみます。
  4> for i in [7, 8, 9, 2].lazy.map({x->Int in print(x); return x * 100}) {
  5.     print(i) }
7
700
8
800
9
900
2
200
  6>  
これを lazy 無しの場合と比べてください。
  6> for i in [7, 8, 9, 2].map({x->Int in print(x); return x * 100}) {
  7.     print(i) }
7
8
9
2
700
800
900
200
  8>  
こちらはループ本体に入る前に map メソッドが関数引数をすっかり呼び出し終わっています。
LINQ の Where メソッドは LazySequenceType の filter メソッドで代用できます。 平たく言えば filter の代わりに lazy.filter を使えば OK です。 例を示します。
  8> for i in [7, 8, 9, 2].lazy.filter({$0 % 2 == 0}) { print(i) }
8
2
  9>  
これもどこがどう遅延しているのかよく分かりませんから print をさしはさんで lazy 有りと無しで比べてみます。
  9> for i in [7, 8, 9, 2].lazy.filter({x->Bool in print("- \(x)");
 10.         return x % 2 == 0}) { print(i) }
- 7
- 8
8
- 9
- 2
2
 11> for i in [7, 8, 9, 2].filter({x->Bool in print("- \(x)");
 12.         return x % 2 == 0}) { print(i) }
- 7
- 8
- 9
- 2
8
2
 13>  

3.2 takeWhile メソッド

列の要素を述語関数に適用して false になったら打ち切る takeWhile メソッドは次のように定義できる。 打ち切りは,新しい列の next メソッドが nil を返すことで行う。

    /// 先頭からの要素を predicate が真である限りの間だけ返す。
    func takeWhile(predicate: E->Bool) -> AnySequence<E> {
        return AnySequence { () -> AnyGenerator<E> in
            var g = self.generate()
            return anyGenerator { () -> E? in
                if let e = g.next() {
                    if predicate(e) {
                        return e
                    } else {
                        return nil
                    }
                } else {
                    return nil
                }
            }
        }
    }

関数引数 predicate の型が E throws -> Bool ではないことに注意されたい。 エラーを送出する関数を引数として与えることはできない。 これについては本稿の最後,4.1節で議論する。

takeWhile メソッドの使用例を示す。この例では 9 以上の要素が出現したらループを打ち切る。

 13> for i in [7, 8, 9, 2].takeWhile({$0 < 9}) { print(i) }
7
8
 14>  
動作を確かめるために print をさしはさんでみます。
 14> for i in [7, 8, 9, 2].takeWhile({x->Bool in print("- \(x)"); return x < 9}
 15.     ) { print(i) }
- 7
7
- 8
8
- 9
 16>  
このように毎回のループごとに1要素ずつ打ち切るかどうかを判定しています。

3.3 skipWhile メソッド

列の要素を述語関数に適用して false になったところから始める skipWhile メソッドは次のように定義できる。 いったん false になって先頭要素が得られたらもう述語関数は使われない。 この切り替えをフラグ変数 first を使って実現する。

    /// 先頭からの要素を predicate が真である限りの間だけ除いて返す。
    func skipWhile(predicate: E->Bool) -> AnySequence<E> {
        return AnySequence { () -> AnyGenerator<E> in
            var g = self.generate()
            var first = true
            return anyGenerator { () -> E? in
                if first {
                    first = false
                    for ;; {
                        if let e = g.next() {
                            if !predicate(e) {
                                return e
                            }
                        } else {
                            return nil
                        }
                    }
                }
                if let e = g.next() {
                    return e
                } else {
                    return nil
                }
            }
        }
    }

使用例を示す。この例では 9 以上の要素が出現したら列挙を開始する。

 16> for i in [7, 8, 9, 2].skipWhile({$0 < 9}) { print(i) }
9
2
 17>  
skipWhile は条件を満たす要素を見つけた後はもう関数引数を使いませんから,今までのように関数引数の中で print を使って動作を確かめる方法が有効に働きません。 要素を見つけた後,残りの要素をまとめて取得するのではなく遅延的にそのつど取得していることを確かめるためには lazy.map に skipWhile を連鎖させて map の関数引数の中で print します。
 17> for i in [7, 8, 9, 2].lazy.map({x->Int in print("- \(x)"); return x}
 18.     ).skipWhile({$0 < 9}) { print(i) }
- 7
- 8
- 9
9
- 2
2
 19>  
確かに毎回のループごとに列の要素を取得していることが分かります。 特に実行例は示しませんが,3.4 節以降のメソッドも同様にして動作を確かめられます。

3.4 take メソッド

列の要素を先頭から指定個数だけ採る take メソッドは次のように定義できる。 変数 i を使って指定個数までカウントする。

    /// 列の最初の count 個の要素からなる列を返す。
    func take(count: Int) -> AnySequence<E> {
        return AnySequence { () -> AnyGenerator<E> in
            var g = self.generate()
            var i = 0
            return anyGenerator { () -> E? in
                if i < count {
                    if let e = g.next() {
                        i++
                        return e
                    }
                }
                return nil
            }
        }
    }

使用例を示す。

 19> for i in [7, 8, 9, 2].take(3) { print(i) }
7
8
9
 20> for i in (1...10).take(3) { print(i) }
1
2
3
 21>  

3.5 skip メソッド

列の要素を先頭から指定個数だけ除く skip メソッドは次のように定義できる。 指定個数だけ除いた先頭要素を返すまでの処理と,それ以降の処理をフラグ変数 first で切り替える。

    /// 列の最初の count 個の要素を除いた列を返す。
    func skip(count: Int) -> AnySequence<E> {
        return AnySequence { () -> AnyGenerator<E> in
            var g = self.generate()
            var first = true
            return anyGenerator { () -> E? in
                if first {
                    first = false
                    for var i = 0; i < count; i++ {
                        if g.next() == nil {
                            return nil
                        }
                    }
                }
                if let e = g.next() {
                    return e
                } else {
                    return nil
                }
            }
        }
    }

使用例を示す。

 21> for i in (1...10).skip(3) { print(i) }
4
5
6
7
8
9
10
 22>  
LINQ の SelectMany メソッドは LazySequenceType の flatMap メソッドで代用できます。 例を示します。
 22> let owners = [("太郎", ["コロ", "ポチ"]), ("次郎", ["クロ", "ブチ"])]
owners: [(String, [String])] = 2 values {
  [0] = {
    0 = "太郎"
    1 = 2 values {
      [0] = "コロ"
      [1] = "ポチ"
    }
  }
  [1] = {
    0 = "次郎"
    1 = 2 values {
      [0] = "クロ"
      [1] = "ブチ"
    }
  }
}
 23> for s in owners.lazy.flatMap({$0.1}) { print(s) }
コロ
ポチ
クロ
ブチ
 24>  

3.6 concat メソッド

二つの列を連接した新しい列を作る concat メソッドは次のように定義できる。 二つの列 self と other は,それぞれの要素の型 E と S.Generator.Element は等しいが,ジェネレータの型が等しいとは限らない。 型の整合性を満たすため,ここでは単純に二つのジェネレータ変数 g1 と g2 を設ける。

    /// 列と列をつなげた列を返す。
    func concat<S: SequenceType where S.Generator.Element == E>
        (other: S) -> AnySequence<E> {
        return AnySequence { () -> AnyGenerator<E> in
            var g1 = self.generate()
            var g2: S.Generator? = nil
            return anyGenerator { () -> E? in
                if g2 == nil {
                    if let e = g1.next() {
                        return e
                    } else {
                        g2 = other.generate()
                    }
                }
                if let e = g2!.next() {
                    return e
                } else {
                    return nil
                }
            }
        }
    }

使用例を示す。

 24> for i in (1...3).concat(101...103) { print(i) }
1
2
3
101
102
103
 25>  
LINQ の Zip メソッドは zip 関数と LazySequenceType の map メソッドの組み合わせで代用できます。 例を示します。
 25> for s in zip((1...5), ["a", "b", "c"]).lazy.map({"\($0)-\($1)"}) {
 26.     print(i) }
1-a
2-b
3-c
 27>  
ちなみに LINQ の Aggregate メソッドは SequenceType の reduce メソッドで代用できます。 例を示します。
 27> (1...10).reduce(0, combine: {$0 + $1})
$R0: Int = 55
 28> (1...10).lazy.reduce(0, combine: {$0 + $1})
$R1: Int = 55
 29>  
reduce メソッドは列の要素を次々と消費して結果を算出しますから,遅延的かどうかは reduce の動作には関係しないことに注意してください。

4. おわりに

前回前々回の試みと同じく今回の LINQ 実装も C# のオリジナル版と同じ計算量を実現する。 例えば,下記のプログラムは途中で配列を作ることなくループ1回につき1要素だけ算出して遂行される。

import Linq

// フィボナッチ数列
let fib = AnySequence { () -> AnyGenerator<Int> in
    var a = 0, b = 1
    return anyGenerator { () -> Int? in
        (a, b) = (b, a + b)
        print("- \(a)")         // 途中経過を - 付きで印字する。
        return a
    }
}

// 500 未満を印字する。
for e in fib.takeWhile({$0 < 500}) {
    print(e)
}

実行例を示す。

01:~/tmp$ swift -I. -L. -lLinq fib_test.swift
- 1
1
- 1
1
- 2
2
- 3
3
- 5
5
- 8
8
- 13
13
- 21
21
- 34
34
- 55
55
- 89
89
- 144
144
- 233
233
- 377
377
- 610
01:~/tmp$  

4.1 未解決の課題

未解決の課題はエラーを引き起こし得る関数引数の扱いである。

現行の Swift では関数型に throws を付けることによって,その関数がエラーを送出し得ることを示すことができる。 SequenceType の非遅延版 map メソッドや filter メソッドは throws 付きの関数引数を受理し,かつメソッドそれ自身も rethrows 付きと宣言されている。 メソッドを評価したとき,実行結果として配列が得られるか,それともメソッド内部で関数引数の呼び出しが引き起こしたエラーの再送出 (rethrow) に終わるか,どちらかである。

その一方,LazySequenceType の map メソッドや filter メソッドは throws 付きの関数引数を受理しない。 これらのメソッドの評価結果はいわば遅延列 (lazy sequence) であり,関数引数の呼び出しは,この遅延列が作り出すジェネレータの next メソッドの実行時に行われる。 しかし,現行の Swift の GeneratorType の next メソッドの型は () -> Self.Element? であって () throws -> Self.Element? ではないから,ここでエラーを送出することはできない。 LazySequenceType の map メソッド等が throws 付きの関数引数を受理しないのはこのためである。

同様の理由により,今回与えた takeWhile, skipWhile 両メソッドでも throws 付きの関数引数 predicate: E throws -> Bool を無理なく受理させるようには定義できない。

定義できない直接の理由はメソッド実装で使われる anyGenerator 関数が throws 付きの関数引数を受理しないことによるコンパイル時のエラーです。 もちろん,関数引数を try predicate(e) ではなく try! predicate(e) として扱うようにすればコンパイルは通ります。 これが上記で「無理なく」と限定した理由です。 しかし,それで無理にコンパイルだけ通したからといって,むしろ危険なだけです。

これらのメソッドが throws 付きの関数引数を受理できるようにするためには,正攻法で考えれば Swift の GeneratorType の next メソッドの改訂が必要である。 それに伴って for-in 文の意味論と実装の改訂も必要である。 今や Swift はオープンソース化されているから,こうした改造は決して不可能ではない。 しかし,そのようにして言語自体を改訂するだけの有用性があるかどうか自明ではない。 エラーの扱いは未解決の課題である。


Copyright (c) 2015 OKI Software Co., Ltd.