こんにちは。よっしーです(^^)
今日は、Golangにおけるジェネリクスの基本をご紹介します。
概要
ジェネリクスを使うと、呼び出し元のコードから提供される一連の型のどれでも動作するように書かれた関数や型を宣言して使うことができます。
この記事では、2つの単純な非ジェネリクス関数を宣言し、同じロジックを1つのジェネリクス関数に取り込みます。
下記のセクションで構成されています。
- コード用のフォルダを作成する。
- 非ジェネリクス関数を追加します。
- 複数の型を扱うジェネリクス関数を追加します。
- ジェネリクス関数を呼び出す際に型引数を削除します。
- 型制約を宣言します。
コード用のフォルダを作成する
最初に作業ディレクトリを作成します。手順は下記のコマンドになります。
mkdir 15_learn-golang-generics
cd 15_learn-golang-generics
asdfコマンドで使用するGolangのバージョンを指定します。すでにGolangが実行できる状態にある方は、このコマンドをスキップしても問題ありませんが、Go 1.18以降である必要があります。
asdf local golang 1.20.5
モジュールの初期化を初期化します。この例では、genericsモジュールを作成します。
go mod init example/generics
非ジェネリクスの関数を追加する
このセクションでは、マップの値を足して合計を返す関数を2つ追加します。
1つではなく2つの関数を宣言しているのは、int64の値を格納するマップとfloat64の値を格納するマップの2つの異なるタイプのマップを扱うためです。
テキストエディタを使って、作業ディレクトリにmain.goというファイルを作成します。このファイルにGoのコードを書いていきます。
main.goのファイルの一番上に、以下のパッケージ宣言を貼り付けます。
package main
パッケージ宣言の下に、以下の2つの関数宣言を貼り付けます。
// SumInts adds together the values of m.
func SumInts(m map[string]int64) int64 {
var s int64
for _, v := range m {
s += v
}
return s
}
// SumFloats adds together the values of m.
func SumFloats(m map[string]float64) float64 {
var s float64
for _, v := range m {
s += v
}
return s
}
main.goの一番上、パッケージ宣言の下に、以下のmain関数を貼り付け、2つのマップを初期化し、前のステップで宣言した関数を呼び出す際の引数として使用する。
func main() {
// Initialize a map for the integer values
ints := map[string]int64{
"first": 34,
"second": 12,
}
// Initialize a map for the float values
floats := map[string]float64{
"first": 35.98,
"second": 26.99,
}
fmt.Printf("Non-Generic Sums: %v and %v\n",
SumInts(ints),
SumFloats(floats))
}
main.goの一番上、パッケージ宣言のすぐ下に、今書いたコードをサポートするために必要なパッケージをインポートします。
コードの最初の行は次のようにします。
package main
import "fmt"
main.goを保存します。
main.goのあるディレクトリのコマンドラインから、コードを実行する。
go run .
下記のような出力になっていれば成功です。
% go run .
Non-Generic Sums: 46 and 62.97
複数のタイプを扱うジェネリクス関数を追加
ジェネリクス関数を使えば、ここに2つの関数を書く代わりに1つの関数を書くことができます。次に、整数値または浮動小数点値を含むマップのジェネリクス関数を1つ追加します。
このセクションでは、整数値または浮動小数点値を含むマップを受け取ることができるジェネリクス関数を1つ追加します。
どちらの型の値もサポートするためには、その単一の関数がどの型をサポートするかを宣言する方法が必要になります。一方、呼び出し側のコードには、整数マップと浮動小数点マップのどちらで呼び出すかを指定する方法が必要になります。
これをサポートするために、通常の関数パラメータに加えて型パラメータを宣言する関数を書きます。これらの型パラメータは関数を汎用的なものにし、異なる型の引数を扱うことを可能にします。型引数と通常の関数引数で関数を呼び出します。
各型パラメータには、型パラメータのメタ型のような働きをする型制約があります。各型制約は、呼び出しコードがそれぞれの型パラメータに対して使用できる許容される型引数を指定します。
型パラメータの制約は通常、型の集合を表しますが、コンパイル時には、型パラメータは単一の型(呼び出し元のコードによって型引数として提供される型)を表します。型引数の型が型パラメータの制約で許可されていない場合、コードはコンパイルされません。
型パラメータは、ジェネリック・コードがそれに対して実行するすべての操作をサポートしなければならないことに注意してください。例えば、関数のコードが、制約に数値型が含まれている型パラメータに対して文字列操作(インデックス付けなど)を実行しようとした場合、コードはコンパイルされません。
これから書くコードでは、integer型かfloat型のどちらかを許容する制約を使用します。
先に追加した2つの関数の下に、以下の汎用関数を貼り付けます。
// SumIntsOrFloats sums the values of map m. It supports both int64 and float64
// as types for map values.
func SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V {
var s V
for _, v := range m {
s += v
}
return s
}
main.goの、すでにあるコードの下に、以下のコードを貼り付ける。
fmt.Printf("Generic Sums: %v and %v\n",
SumIntsOrFloats[string, int64](ints),
SumIntsOrFloats[string, float64](floats))
main.goのあるディレクトリのコマンドラインから、コードを実行する。
go run .
下記のような出力になっていれば成功です。
% go run .
Non-Generic Sums: 46 and 62.97
Generic Sums: 46 and 62.97
コードを実行するために、コンパイラーはそれぞれの呼び出しで、型パラメーターをその呼び出しで指定された具象型に置き換えます。
ジェネリック関数を呼び出す際、あなたは型引数を指定し、関数の型パラメーターの代わりにどの型を使用するかをコンパイラーに伝えます。次のセクションで説明するように、多くの場合、コンパイラは型引数を推測することができるので、型引数を省略することができます。
ジェネリック関数を呼び出す際の型引数の削除
このセクションでは、ジェネリクス関数呼び出しの修正バージョンを追加し、呼び出しコードを簡素化するために小さな変更を加えます。この場合、型引数は不要なので削除します。
Goコンパイラが使用したい型を推測できる場合、呼び出しコードで型引数を省略することができます。コンパイラーは関数の引数の型から型引数を推測します。
これは常に可能というわけではないことに注意してください。たとえば、引数のないジェネリクス関数を呼び出す必要がある場合は、関数呼び出しに型引数を含める必要があります。
main.goの、すでにあるコードの下に、以下のコードを貼り付けます。
fmt.Printf("Generic Sums, type parameters inferred: %v and %v\n",
SumIntsOrFloats(ints),
SumIntsOrFloats(floats))
main.goのあるディレクトリのコマンドラインから、コードを実行する。
go run .
下記のような出力になっていれば成功です。
% go run .
Non-Generic Sums: 46 and 62.97
Generic Sums: 46 and 62.97
Generic Sums, type parameters inferred: 46 and 62.97
型制約の宣言
この最後のセクションでは、先ほど定義した制約を独自のインターフェースに移動して、複数の場所で再利用できるようにします。このように制約を宣言することは、制約がより複雑になる場合など、コードの合理化に役立ちます。
型制約をインターフェースとして宣言します。制約は、インターフェースを実装する任意の型を許可します。例えば、3つのメソッドを持つ型制約インターフェースを宣言し、それをジェネリック関数の型パラメータで使用する場合、関数を呼び出すために使用される型引数は、それらのメソッドをすべて持っていなければなりません。
このセクションで見るように、制約インターフェースは特定の型を参照することもできます。
mainのすぐ上、import文の直後に以下のコードを貼り付け、型制約を宣言します。
type Number interface {
int64 | float64
}
すでにある関数の下に、以下の汎用SumNumbers関数を貼り付ける。
// SumNumbers sums the values of map m. It supports both integers
// and floats as map values.
func SumNumbers[K comparable, V Number](m map[K]V) V {
var s V
for _, v := range m {
s += v
}
return s
}
main.goの、すでにあるコードの下に、以下のコードを貼り付けます。
fmt.Printf("Generic Sums with Constraint: %v and %v\n",
SumNumbers(ints),
SumNumbers(floats))
main.goのあるディレクトリのコマンドラインから、コードを実行する。
go run .
下記のような出力になっていれば成功です。
% go run .
Non-Generic Sums: 46 and 62.97
Generic Sums: 46 and 62.97
Generic Sums, type parameters inferred: 46 and 62.97
Generic Sums with Constraint: 46 and 62.97
解説
SumIntsOrFloatsメソッド
このコードは、与えられたマップの値の合計を計算する関数 SumIntsOrFloats
を定義しています。この関数は、マップの値の型として int64
または float64
をサポートしています。
関数のシグネチャは以下のようになっています:
func SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V
この関数は、ジェネリックな型パラメータ K
と V
を持ちます。K
はマップのキーの型を表し、V
はマップの値の型を表します。comparable
はキーの型が比較可能であることを示す制約です。V
の型は int64
または float64
であることが制約されています。
関数の処理は以下のようになっています:
- 変数
s
をV
型のゼロ値で初期化します。V
はint64
またはfloat64
のいずれかです。 - マップ
m
の各値v
に対して、ループを実行します。キーは無視されます(_
に代入)。 s
に値v
を加算します。- 最終的な合計値
s
を返します。
この関数を使うと、与えられたマップの値の合計を計算することができます。
SumNumbersメソッド
このコードは、与えられたマップの値の合計を計算する関数 SumNumbers
を定義しています。この関数は、マップの値の型として整数または浮動小数点数をサポートしています。
まず、Number
という名前のインターフェースを定義しています。
type Number interface {
int64 | float64
}
このインターフェースは、int64
型または float64
型のいずれかを実装する型を表しています。つまり、Number
型としては整数または浮動小数点数が使えます。
次に、関数 SumNumbers
のシグネチャが続きます:
func SumNumbers[K comparable, V Number](m map[K]V) V {
// 関数の本体
}
この関数は、ジェネリックな型パラメータ K
と V
を持ちます。K
はマップのキーの型を表し、V
はマップの値の型を表します。comparable
はキーの型が比較可能であることを示す制約です。V
の型は Number
インターフェースを実装した整数または浮動小数点数の型であることが制約されています。
関数の処理は以下のようになっています:
- 変数
s
をV
型のゼロ値で初期化します。V
はNumber
インターフェースを実装した整数または浮動小数点数の型です。 - マップ
m
の各値v
に対して、ループを実行します。キーは無視されます(_
に代入)。 s
に値v
を加算します。- 最終的な合計値
s
を返します。
この関数を使うと、与えられたマップの値の合計を計算することができます。
このように、SumNumbers
関数を使用することで、整数と浮動小数点数の両方をサポートする柔軟なマップ合計の計算が可能になります。
おわりに
今日は、Golangにおけるジェネリクスについてご紹介しました。
本記事で使用したコードは下記のリポジトリにあります。
Golangが初めての方は、Effective Go と How to write Go codeに役立つベストプラクティスが記載されています。
また、Go ツアーというGolang の基礎をステップバイステップで学べる入門サイトもあります。
何か質問や相談があれば、遠慮なくコメントしてください。また、エンジニア案件についても、いつでも相談にのっていますので、お気軽にお問い合わせください。
それでは、また明日お会いしましょう(^^)
コメント