LoginSignup
10
10

More than 5 years have passed since last update.

Swiftでタイプそのものを引数にとる関数を実装してみる.

Last updated at Posted at 2014-08-15

バイナリファイルから整数値を取り出すようなことがある場合,バイト配列を整数に変換する関数を用意しておくと便利です.このときの関数はgetInt32()getUInt64()などと整数のタイプごとに別個に実装するかと思います.実装の中身はほとんど同じであるにも関わらずです.
そこで,綺麗に実装を一つにまとめられないものだろうかと思い,ジェネリクスを使って試してみました.個人的にはなかなか面白い作業だったと思うので,ここにログしておきます.
 

今回,仕立てた関数getIntegerはこのように使います.

let getInt16 = getInteger(type: Int16.self)
let getUInt32 = getInteger(type: UInt32.self)

var v = getInt16(bytes: someByteArray)
var w = getUInt32(bytes: someByteArray)

整数タイプを指定するとそれ用の関数が返ってきます.あとは,バイト配列を放り込めばお望みの整数が出力されるという寸法です.
(タイプ自身を表すのに.selfを付けないといけないのがイマイチな感じはしますね...)

 
ところで,今回もextensionしまくるわけですが,'The Swift Programmig Language'のAccess Controlの最後の方をみると,protocolやextensionもアクセスコントロールの対象にできるとあります.
組み込みタイプを"魔改造"しすぎてもprivateprotocolextensionに付けておけば,波及をソースファイル内に封じ込めることができるという.これ,すごく便利そうですね.魔改造ファンにはもってこいの機能です.最終的に仕上がった関数なりクラスなりをpublicinternalにしておけばよいというわけです.
 

さて本題です.
getIntegerを実装するには,まず,次のような単純な関数が動くような仕込みが必要になります.

func foo<T>(x: Int8, y: T) -> T {
    return T(x) << y
}

ここで,タイプTInt32など整数タイプのどれかのつもりです.
しかし,コンパイラ様は2点ほど文句つけてきます.

  • タイプTはイニシャライザを持ってないよ.
  • タイプTに適用できるシフト演算子はないよ.

なるほど.ないのであれば,新たに仕込みましょう.
組み込みの整数タイプ(Int8など全10種)を調べてみると,全て共通して各整数タイプ用のイニシャライザを持っていますが,なんらかのプロトコルで強制されているわけではありません.プロトコルInitializableをでっち上げて強制してしまいしましょう.

protocol Initializable {
    init(_ v: Int8)
    init(_ v: UInt8)
    //... ...          全部で10個のinit
    init(_ v: UInt)
}

extension Int8: Initializable {}
//... 他の9種もextension

コンパイラはこの空っぽのextensionでも文句はつけません.すでに全initが実装されてるからですね.
そして関数foo

func foo<T: Initializable>(x: Int8, y: T) -> T {...}

と記述し直し,「TはプロトコルInitializableを満たすタイプである」との制約を付けると,1点目のエラーは解消します.
 
2点目ですが,タイプT用の<<の実装は少々汚くなります.次のような簡略版でお茶を濁しておくこともできますが... (タイプTIntegerTypeに制約しておく必要があります.)

func <<<T: IntegerType>(lhs: T, rhs: T) -> T {
    var n = lhs
    for _ in 0 ..< rhs {
        n = n &* 2  // overflow? dont' care
    }
    return n
}

こんなループでぐるぐるなシフト演算は使いたくないですね.

 
真面目に<<を実装するには,タイプTを実際の本当のタイプ,例えばInt16に変換してやる必要がありそうです.そうしないと組み込み済の<<を利用できないので.
整数の具体的なタイプはサイズと符号の有無で特定できそうです.サイズはsizeof(T)で取れますが,組み込み整数タイプは符号の有無を取れるプロパティ/メソッドを持ってないので新たにプロトコルSignCheckableを作って各タイプにextensionしてしまいます.オブジェクトではなくタイプに問い合わせるのでstaticなプロパティにしてます(整数タイプはclassでなくstruct).

protocol SignCheckable {
    class var isSignedType: Bool {get}
}

extension Int16: SignCheckable  {
    static var isSignedType: Bool {return true}
}
// ... その他のタイプもextension

次に必要なのは,具象タイプへの'init'の追加かファクトリメソッドです.
実は,initの実装でもTの実際のタイプを特定しないとダメで,そうなると,<<関数の中でもタイプを特定し,そこからinitの中でも再び特定しと少々バカらしいのでinitはやめておきましょう.ファクトリにしておきましょう.これもプロトコルCreatableを作ってextensionです.

protocol Creatable {
    class func createFrom<T>(v: T) -> Self
}

extension Int16: Creatable {
    static func createFrom<T>(v: T) -> Int16 {
        return Int16(v as Int16)
    }
}
// ... 他のタイプもextension

プロトコル内では自分自身のタイプをSelfで表現するようです.createFromメソッド内でv as Int16とやっていますが,必ずこのキャストは成功します.なぜなら,今回の使い方(<<演算子用)ではvの実際のタイプはInt16だからです.他の整数タイプでも同様です.なんだか回りくどいですね.自分自身でダブルディスパッチみたいになってます.しかし,こうやらないとTからInt16への変換ができないんですよね.もっと簡単な方法があるかもしれませんが思いつきません.

これでやっと<<関数(演算子)を定義できます.以下のようになります.プロトコルがいくつもあるとTへの制約がだらだらと長くなってしまうので.上で作ったプロトコルをまとめて新たにプロトコルMyIntegerTypeを作ってます.

private func <<<T: MyIntegerType>(lhs: T, rhs: T) -> T {
    switch (T.isSignedType, sizeof(T)) {
    //...
    case (true, 2):
        let (m, n) = (T.createFrom(lhs) as Int16,
                      T.createFrom(rhs) as Int16)
        return T(m << n)
    //...
    case (false, 4):
        let (m, n) = (T.createFrom(lhs) as UInt32,
                      T.createFrom(rhs) as UInt32)
        return T(m << n)
    default:
        // error...
    }
}

switch使って符号の有無とサイズのパターンを網羅します.各case内でタイプTを,マッチする整数の具象タイプに変換します.ここでもas Int16などが必要になります.コンパイル時にはcreateFromからの戻り値タイプが分からないんです.そして,その具象タイプ用の組み込み<<演算子を使ってシフト演算します.いや〜,汚いですね.なんかいい方法はないものでしょうか?
汚いのはともかく,なんとかタイプT用の<<関数が実装できたので,上の方で定義した関数fooT: MyIntegerTypeという制約を付ければコンパイラも真っ当な関数として扱ってくれるようになります.


 
 
以上で,お膳立ては整いました.やっとgetInteger関数を定義できます.といっても,大したものじゃありません.基本的にシフトと論理和だけが仕事ですから.

func getInteger<T where T: IntegerType, T: MyIntegerType>
(# type: T.Type)(bytes: [Int8]) -> T
{
    let (shift, indices) = {(t: T.Type) -> (T, [T]) in
        switch sizeof(t) {
        case 1: return ( 0, [0])
        case 2: return ( 8, [0,1])
        case 4: return (24, [0,1,2,3])
        case 8: return (56, [0,1,2,3,4,5,6,7])
        default:
            return (0, []) // error...
        }
    }(type)

    if shift == 0 {
        return type.isSignedType ? bytes[0] as T : UInt8(bitPattern: bytes[0]) as T
    }

    let b = bytes.map(indices) {
        (s: Int8, t: T) -> T in
        let v = T(Int16(s) & 0xff)
        return  v << (shift - t * 8)
    }
    return b.reduce(0) {$0 | $1}
}

getIntegerは後の使い勝手を考えカリー化して定義しています.最初の引数に整数のタイプを指定.タイプを表す識別子はT.Typeとなるようですね.2番目の引数は整数を切り出す元となるバイト列です.
関数の中身は,シフトする量と,直後のmap関数で使うインデックス用の配列を生成します.個人的好みで,letとクロージャ定義&即起動をやってます(使ってみたかった).タプルのパターンマッチで複数変数を一遍に初期化できるのは気持ちいいです.
map関数は,map関数の引数を増やしてしまおうとだいたい同じノリのものです.なんでmapに拘るかといえば,map&reduceでズバッといきたいからでした.
mapでシフト演算してバイトの位置を定めてからreduceで一気に論理和をという,関数型の典型パターン.
あとは,

let getInt32 = getInteger(Int32.self)
let getUInt64 = getInteger(UInt64.self)
...

などとgetIntegerをタイプで部分適用した定数を並べて目的達成です.
ちなみに全部ビッグエンディアンになってます.

 
正直,ここまで面倒なお膳立てが必要になるとは思いもしませんでしたが,Swiftのジェネリクスの勉強にもなったし良しとしましょう.性能が気になるところですが,Swiftが正式リリースされるまではパフォーマンス計測はしないでおこうと思います.
組み込みタイプの魔改造が終わった後は安全のために一つのファイルに切り出してプロトコル等はprivateにしておいたほうが良いでしょうね.

10
10
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
10