Goを実際に使用している、または実際に使用したい多くのプログラマーにとって、言語にパラメトリック多型のメカニズムがないことは大きな悲しみです。 しかし、すべてが一見すると思われるほど悪いわけではありません。
もちろん、Goで一般化されたプログラムを、たとえばC ++テンプレートのスタイルで作成することはできません。これは実際にはCPU時間に影響を与えません。 言語にはそのようなメカニズムはなく、予期されていない可能性が十分にあります。
一方、この言語は、オブジェクトと関数の両方を反映できる、かなり強力な組み込みパッケージreflect
を表しています。 最前線でスピードを出さない場合、このパッケージを使用すると、興味深く柔軟なソリューションを実現できます。
この記事では、 for each
型に依存しない再帰関数として実装for each
方法を示します。
問題
Goでは、 for range
構造体を使用して、コレクションの要素( Array
、 Slice
、 String
)を反復処理します。
for i, item := range items { // do something }
同様に、 Channel
からアイテムを選択できます:
for item := range queue { // do something }
一般に、これはfor eachループのニーズの80%をカバーします。 ただし、組み込みのfor range
設計には、小さな例を使って簡単に説明できる落とし穴があります。
Car
とBike
2つの構造があるとします(自動車店のコードを書いていると想像してください):
type Car struct{ Name string Count uint Price float64 } type Bike struct{ Name string Count uint Price float64 }
在庫があるすべての車とオートバイのコストを計算する必要があります。
これを1つのループで行うには、Goでフィールドアクセスを一般化する新しい型が必要です。
type Vehicle interface{ GetCount() uint GetPrice() float64 } func (c Car) GetCount() uint { return c.Count; } func (c Car) GetPrice() float64 { return c.Price; } func (b Bike) GetCount() uint { return b.Count; } func (b Bike) GetPrice() float64 { return b.Price; }
これで、 for range
を使用for range
vehicles
バイパスを構成することにより、総コストを計算できます。
vehicles := []Vehicle{ Car{"Banshee ", 1, 10000}, Car{"Enforcer ", 3, 15000}, Car{"Firetruck", 4, 20000}, Bike{"Sanchez", 2, 5000}, Bike{"Freeway", 2, 5000}, } total := float64(0) for _, vehicle := range vehicles { total += float64(vehicle.GetCount()) * vehicle.GetPrice() } fmt.Println("total", total) // $ total 155000
毎回ループを書かないために、入力型[]Vehicle
を取り、数値結果を返す関数を書くことができます:
func GetTotalPrice(vehicles []Vehicle) float64 { var total float64 for _, vehicle := range vehicles { total += float64(vehicle.GetCount()) * vehicle.GetPrice() } return total }
奇妙なことに、このコードを個別の関数として分離することにより、柔軟性が失われます。 次の問題が表示されます。
- 要素の厳密な入力を制限します。 なぜなら for / range構造は強く型付けされており、要素タイプをキャストしないため、関数シグネチャで予期される要素タイプを明示的に指定する必要があります。 結果として、
[]Car
または[]Bike
スライスを直接渡すことはできませんが、Car
とBike
両方のタイプはVehicle
インターフェースの条件を満たす:
cars := []Car{ Car{"Banshee ", 1, 10000}, Car{"Enforcer ", 3, 15000}, Car{"Firetruck", 4, 20000}, } fmt.Println("total", GetTotalPrice(cars)) // Compilation error: cannot use cars (type []Car) as type []Vehicle in argument to GetTotalPrice
- コレクションの厳密な入力を制限します。 たとえば、
[]Vehicle
スライスの代わりに、map[int]Vehicle
辞書を渡すことはできません。
cars := map[int]Vehicle{ 1: Car{"Banshee ", 1, 10000}, 2: Car{"Enforcer ", 3, 15000}, 3: Car{"Firetruck", 4, 20000}, } fmt.Println("total", GetTotalPrice(cars)) // Compilation error: cannot use vehicles (type map[int]Vehicle) as type []Vehicle in argument to GetTotalPrice
言い換えれば、for / rangeでは、コードの任意の部分を選択して、柔軟性を失わずに関数にラップすることはできません。
解決策
タイピングが厳密な多くの言語で説明されている問題は、パラメトリック多型のメカニズム(ジェネリック、テンプレート)を使用して解決されます。 しかし、パラメトリックポリフォームの見返りとして、Goの著者は、リフレクションのメカニズムreflect
実装する組み込みパッケージreflect
提示しました。
一方では、リフレクションはよりリソース集約型のソリューションですが、他方では、より柔軟でインテリジェントなアルゴリズムを作成できます。
タイプ反射(reflect.Type)
実際、 reflect
パッケージには2つのタイプのリフレクションがあります-これはreflection.Typeタイプreflect.Type
とreflection.Value value reflect.Value
です。 型のリフレクションは型のプロパティのみを記述するため、同じ型を持つ2つの異なる変数は同じ型のリフレクションを持ちます。
var i, j int var k float32 fmt.Println(reflect.TypeOf(i) == reflect.TypeOf(j)) // true fmt.Println(reflect.TypeOf(i) == reflect.TypeOf(k)) // false
なぜなら Go型は基本型に基づいて構築されるため、分類用のKind型を持つ特別な列挙があります。
const ( Invalid Kind = iota Bool Int Int8 Int16 Int32 Int64 Uint Uint8 Uint16 Uint32 Uint64 Uintptr Float32 Float64 Complex64 Complex128 Array Chan Func Interface Map Ptr Slice String Struct UnsafePointer )
したがって、タイプreflect.Type
リフレクションにアクセスすると、変数の完全なタイプを判別せずに、ディスパッチを許可するタイプの種類を常に見つけることができます。 たとえば、変数が関数であることを知るだけで十分です。この関数が持つ特定の型の詳細には触れません。
valueType := reflect.TypeOf(value) switch valuteType.Kind() { case reflect.Func: fmt.Println("It's a function") default: fmt.Println("It's something else") }
便宜上、変数の型のリフレクションを同じ名前ですが、接尾辞Type
付けて参照します。
callbackType := reflect.TypeOf(callback) collectionType := reflect.TypeOf(collection)
型が属する属に加えて、型リフレクションの助けを借りて、型に関する残りの静的情報(つまり、実行時に変更されない情報)を見つけることができます。 たとえば、ある位置で関数の引数の数と予想される引数のタイプを調べることができます。
if callbackType.NumIn() > 0 { keyType := callbackType.In(0) // expected argument type at zeroth position }
同様に、構造体メンバーの説明にアクセスできます。
type Person struct{ Name string Email string } structType := reflect.TypeOf(Person{}) fmt.Println(structType.Field(0).Name) // Name fmt.Println(structType.Field(1).Name) // Email
配列のサイズは、型を反映することでも認識できます。
array := [3]int{1, 2, 3} arrayType := reflect.TypeOf(array) fmt.Println(arrayType.Len()) // 3
しかし、型の反射によるカットのサイズは、すでに認識できません。 この情報は実行時に変更されます。
slice := []int{1, 2, 3} sliceType := reflect.TypeOf(slice) fmt.Println(sliceType.Len()) // panic!
値の反映(reflect.Value)
型のリフレクションと同様に、Goには値reflect.Value
リフレクションがあり、変数に格納されている特定の値のプロパティを反映します。 これはかなり些細な反射のように見えるかもしれませんが、 Goでは、タイプinterface{}
変数は、関数、数値、構造など、何でも保存できinterface{}
。値のリフレクションは、オブジェクトのあらゆる可能性へのアクセスを多かれ少なかれ安全な形式で表現することを強制されます。 もちろん、これはかなり長いメソッドのリストを生成します。
たとえば、関数のリフレクションを使用して呼び出すことができますreflect.Value
型にreflect.Value
れた引数のリストを渡すだけreflect.Value
。
_callback := reflect.ValueOf(callback) _callback.Call([]reflect.Value{ values })
コレクションのリフレクション(スライス、配列、文字列など)を使用して、要素にアクセスできます。
_collection := reflect.ValueOf(collection) for i := 0; i < _collection.Len(); i++ { fmt.Println(_collection.Index(i)) }
辞書のリフレクションは同じように機能します-バイパスするには、 MapKeys
メソッドでキーのリストを取得し、 MapKeys
要素を選択する必要があります。
for _, k := range _collection.MapKeys() { keyValueCallback(k, _collection.MapIndex(k)) }
構造のリフレクションを使用して、メンバーの値を取得できます。 さらに、メンバーの名前とタイプは、構造のタイプの反映から取得する必要があります。
_struct := reflect.ValueOf(aStructIstance) for i := 0; i < _struct.NumField(); i++ { name := structType.Field(i).Name fmt.Println(name, _struct.Field(i)) }
それぞれの反射ループ
したがって、それぞれに戻った場合、任意の型のコレクションとコールバック関数を受け入れる関数を取得することをお勧めします。そのため、型の一致の責任はユーザーにあります。
なぜなら Goで任意のタイプの関数を渡す唯一の方法は、 interface{}
タイプを指定することです。関数の本体では、 callbackType
タイプのリフレクションに含まれる情報に基づいてチェックを行う必要がありcallbackType
。
- コールバック関数が実際に関数であることを確認します(
calbackType.Kind()
メソッドを使用) - 予想される引数の数を調べる(
callbackType.NumIn()
メソッド) - 失敗した場合は、
panic()
呼び出します
結果は、ほぼ次のコードになります。
func ForEach(collection, callback interface{}) { callbackType := reflect.TypeOf(callback) _callback := reflect.ValueOf(callback) if callbackType.Kind() != reflect.Func { panic("foreach: the second argument should be a function") } switch callbackType.NumIn() { case 1: // Callback expects only value case 2: // Callback expects key-value pair default: panic("foreach: the function should have 1 or 2 input arguments") } }
次に、コレクションを走査するヘルパー関数を設計する必要があります。
型なしの形式ではなく、キーと要素のリフレクションを受け入れる2つの引数を持つ関数の形式でコールバックを渡す方が便利です。
func eachKeyValue(collection interface{}, keyValueCallback func(k, v reflect.Value)) { _collection := reflect.ValueOf(collection) collectionType := reflect.TypeOf(collection) switch collectionType.Kind() { // loops } }
なぜなら 収集通過アルゴリズムは、型リフレクションのKind()
メソッドを介して取得できる種類に依存します。その場合、ディスパッチにはswitch-case
構造を使用すると便利です。
switch collectionType.Kind() { case reflect.Array: fallthrough case reflect.Slice: fallthrough case reflect.String: for i := 0; i < _collection.Len(); i++ { keyValueCallback(reflect.ValueOf(i), _collection.Index(i)) } case reflect.Map: for _, k := range _collection.MapKeys() { keyValueCallback(k, _collection.MapIndex(k)) } case reflect.Chan: i := 0 for { elementValue, ok := _collection.Recv() if !ok { break } keyValueCallback(reflect.ValueOf(i), elementValue) i += 1 } case reflect.Struct: for i := 0; i < _collection.NumField(); i++ { name := collectionType.Field(i).Name keyValueCallback(reflect.ValueOf(name), _collection.Field(i)) } default: keyValueCallback(reflect.ValueOf(nil), _collection) }
コードからわかるように、配列、スライス、および文字列のトラバースは同じ方法で行われます。 辞書、チャネル、および構造には、独自のバイパスアルゴリズムがあります。 コレクションの属がリストされた属のいずれにも該当しない場合、アルゴリズムはコレクション自体をコールバックに渡そうとし、 nil
ポインターのリフレクションがキーとして示されます(呼び出しにIsValid()
を返します)。
これで、コレクションのタイプレストラバーサルを実行する関数が用意されたので、それをクロージャでラップすることにより、 ForEach
関数からの呼び出しに適合させることができます。 これが最終決定です。
func ForEach(collection, callback interface{}) { callbackType := reflect.TypeOf(callback) _callback := reflect.ValueOf(callback) if callbackType.Kind() != reflect.Func { panic("foreach: the second argument should be a function") } switch callbackType.NumIn() { case 1: eachKeyValue(collection, func(_key, _value reflect.Value){ _callback.Call([]reflect.Value{ _value }) }) case 2: keyType := callbackType.In(0) eachKeyValue(collection, func(_key, _value reflect.Value){ if !_key.IsValid() { _callback.Call([]reflect.Value{reflect.Zero(keyType), _value }) return } _callback.Call([]reflect.Value{ _key, _value }) }) default: panic("foreach: the function should have 1 or 2 input arguments") } }
コールバック関数が2つの引数(キー/値ペア)の転送を予期する場合、キーの正確さをチェックする必要があることに注意してください。 彼は無効である可能性があります。 後者の場合、nullオブジェクトはキータイプに基づいて構築されます。
例
次に、私たちのアプローチが提供するものを実証します。 問題に戻ったら、次の方法で解決できます。
func GetTotalPrice(vehicles interface{}) float64 { var total float64 ForEach(vehicles, func(vehicle Vehicle) { total += float64(vehicle.GetCount()) * vehicle.GetPrice() }) return total }
この関数は、記事の冒頭で与えられたものとは対照的に、はるかに柔軟です。 コレクションのタイプに関係なく金額を計算でき、要素のタイプをVehicle
インターフェースにキャストする義務はありません。
vehicles := []Vehicle{ Car{"Banshee ", 1, 10000}, Bike{"Sanchez", 2, 5000}, } cars := []Car{ Car{"Enforcer ", 3, 15000}, Car{"Firetruck", 4, 20000}, } vehicleMap := map[int]Vehicle{ 1: Car{"Banshee ", 1, 10000}, 2: Bike{"Sanchez", 2, 5000}, } vehicleQueue := make(chan Vehicle, 2) vehicleQueue <- Car{"Banshee ", 1, 10000} vehicleQueue <- Bike{"Sanchez", 2, 5000} close(vehicleQueue) garage := struct{ MyCar Car MyBike Bike }{ Car{"Banshee ", 1, 10000}, Bike{"Sanchez", 1, 5000}, } fmt.Println(GetTotalPrice(vehicles)) // 20000 fmt.Println(GetTotalPrice(cars)) // 125000 fmt.Println(GetTotalPrice(vehicleMap)) // 20000 fmt.Println(GetTotalPrice(vehicleQueue)) // 20000 fmt.Println(GetTotalPrice(garage)) // 15000
また、2つの同一サイクルの小さなベンチマークは、柔軟性がどのように達成されるかを明確に示しています。
// BenchmarkForEachVehicles1M total := 0.0 for _, v := range vehicles { total += v.GetPrice() }
//BenchmarkForRangeVehicles1M total := 0.0 ForEach(vehicles, func(v Vehicle) { total += v.GetPrice() })
PASS BenchmarkForEachVehicles1M-2 2000000000 0.20 ns/op BenchmarkForRangeVehicles1M-2 2000000000 0.01 ns/op
おわりに
はい、Goにはパラメトリックなポリフォームはありません。 しかし、パッケージreflect
があり、メタプログラミングの分野で広範な機会を提供します。 reflect
を使用するコードは、典型的なGoコードよりも明らかに複雑に見えます。 一方、反射機能を使用すると、より柔軟なソリューションを作成できます。 これは、たとえばActive Record
概念を実装するときなど、アプリケーションライブラリを記述するときに非常に重要です。
したがって、他のプログラマーがライブラリーをどのように使用するかを事前に知らず、速度の究極が主な目標ではない場合、再帰的なメタプログラミングが最良の選択になる可能性は十分にあります。