Open3

Swiftでのジェネリック(テンプレート)について

kabeyakabeya

Swiftでのジェネリック(テンプレート)について

これどうしたら良いのか

struct Sample<T> {
    var data : [T]
    
    func takeValuesAsInt() -> [Int] {
        var values : [Int] = [Int](repeating: 0, count: data.count)
        for i in (0 ..< data.count) {
            values[i] = Int(data[i])  // ここでエラー
        }
        return values
    }
}

var sample = Sample(data:[Int8](repeating: 1, count: 3))

内部ストレージの型に自由度を持たせるとして、でも何かのタイミングではIntで受け取ろうとするとしたとき、Intに変換しなければならないですね。
これが普通のクラス・構造体あるいはプロトコルなら、Tが何かを示せば良いし、そもそもむやみに型変換しないのだけれども、数値はなかなかそうは言ってられないことがあります。
Intにキャスト・型変換が可能というようなプロトコルが定義されていればできそうなんですが、ExpressibleByIntegerLiteralとか名前は一瞬それっぽい?ものもあったりするものの、整数リテラルで記述可能、ということで微妙にこの目的には使えません。

ちょっと逃げるパターン

struct Sample<T> where T : BinaryInteger {
    var data : [T]
    
    func takeValuesAsInt() -> [Int] {
        var values : [Int] = [Int](repeating: 0, count: data.count)
        for i in (0 ..< data.count) {
            values[i] = Int(data[i])  // OKになる!
        }
        return values
    }
}

var sample = Sample(data:[Int8](repeating: 1, count: 3))

where T: BinaryIntegerを追加したことで、Intへの変換が通るようになります。

ただし今度は、

var sample = Sample(data:[Float](repearing: 1, count: 3))

これがアウトになっちゃう。FloatBinaryIntegerではないからですね。
Swiftの標準ライブラリにおいて、算術演算子の多重定義が異常に多くされていることを考えると、この問題はテンプレート/ジェネリックではなくて、何かの多重定義で逃げるしかないのかも知れません。

メソッドの引数・返値なら多重定義で回避しやすいのだけれども、ストレージの型を可変にする場合、どうしてもクラス・構造体のテンプレートになってしまうので、クラス・構造体ごと分けるとかいうことになってしまいそう。

それでは書く手間がかえって増えてしまいます。そう考えると、こういう設計(ストレージの数値型をテンプレート引数にとる)はそもそも良くないね、という結論になりそうです。

C++だとこんなことにはならない気がします。
(最近いじってないから正確には分かりませんが)

kabeyakabeya
class Sample<T> {
    var data : [T]
    
    init(data: [T]) {
        self.data = data
    }
    
    func takeValuesAsInt() -> [Int] {
        var values : [Int] = [Int](repeating: 0, count: data.count)
        for i in (0 ..< data.count) {
            values[i] = takeValueAsInt(i)
        }
        return values
    }
    
    func takeValueAsInt(_ index: Int) -> Int {
        assert(false, "takeValueAsInt is not implemented")
        return 0
    }
}

class SampleInteger<T> : Sample<T> where T : BinaryInteger {
    override func takeValueAsInt(_ index: Int) -> Int {
        return Int(self.data[index])
    }
}

class SampleFloatingPoint<T> : Sample<T> where T : BinaryFloatingPoint {
    override func takeValueAsInt(_ index: Int) -> Int {
        return Int(self.data[index])
    }
}

var sample1 = Sample(data:[Int8](repeating: 1, count: 3))
var sample2 = SampleInteger(data:[Int8](repeating: 1, count: 3))
//var sample3 = SampleInteger(data:[Float](repeating: 1, count: 3)) // これはコンパイルできない
var sample4 = SampleFloatingPoint(data:[Float](repeating: 1, count: 3))

var values : [Int]
values = sample2.takeValuesAsInt()
print(values)
values = sample4.takeValuesAsInt()
print(values)
values = sample1.takeValuesAsInt()
print(values)

やるとするとこんな感じですかね。structからclassに変更しています。

出力結果
[1, 1, 1]
[1, 1, 1]
__lldb_expr_89/MyPlayground.playground:125: Assertion failed: takeValueAsInt is not implemented

構造体は継承できないのでクラスにするしかないというのと、抽象メソッド・抽象クラスがないので本来インスタンス化されるべきでない基底クラスがインスタンス化されていてもコンパイル時には引っかけられない、というのがちょっと嫌な感じですかね。

構造体を継承させないメリットというのは何となく分かるような気はしますが、抽象メソッドが用意されない理由というのは調べてみたいところです。
(全然違う話になった)

kabeyakabeya

再び話はちょっとずれる

SwiftではObjective-Cのような

- (void)doSomething:(id <MyProtocol>)arg {}

という感じの書き方、例えば

func doSomething(arg: any MyProtocol) {}

のようなことができなくて、C++のように

func doSmothing<T>(arg: T) where T : MyProtocol {}

のように書く、ということのようですね。
ArrayArraySliceのどちらも受け付けられる関数を作ろうとしてはまりました。

Objective-Cから持ち込まれた用語や概念も多いのに、かなりC++に近い言語という気がしますね。
(何を今更、という気はしますが)
それよりも言語の仕様がまだちょくちょく変わっていっているところが、気にはなりますが。