バイナリファイルから整数値を取り出すようなことがある場合,バイト配列を整数に変換する関数を用意しておくと便利です.このときの関数は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もアクセスコントロールの対象にできるとあります.
組み込みタイプを"魔改造"しすぎてもprivate
をprotocol
やextension
に付けておけば,波及をソースファイル内に封じ込めることができるという.これ,すごく便利そうですね.魔改造ファンにはもってこいの機能です.最終的に仕上がった関数なりクラスなりをpublic
かinternal
にしておけばよいというわけです.
さて本題です.
getInteger
を実装するには,まず,次のような単純な関数が動くような仕込みが必要になります.
func foo<T>(x: Int8, y: T) -> T {
return T(x) << y
}
ここで,タイプT
はInt32
など整数タイプのどれかのつもりです.
しかし,コンパイラ様は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
用の<<
の実装は少々汚くなります.次のような簡略版でお茶を濁しておくこともできますが... (タイプT
はIntegerType
に制約しておく必要があります.)
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
用の<<
関数が実装できたので,上の方で定義した関数foo
もT: 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
にしておいたほうが良いでしょうね.