プログラミング
記事内に商品プロモーションを含む場合があります

Go言語のジェネリクス(Generics)の基本的な使い方|Queue, Setを実装してみた

tadanori

Go言語のジェネリクスの使い方について調べてみました。Go言語へのジェネリクスの実装は、比較的新しいものです。これを使うと関数の使い回しが少しだけ楽になります。

ジェネリクスとは

簡単に言うと、ジェネリクス(Generics)は、型をパラメータ化する機能のことです。この機能がある言語では、特定のデータ型に依存しないコードを書くことができます。

例えば、a+bを行う関数addIntを作成したい場合、a, bが整数の場合は、以下のような関数を作成します。

func addInt(a, b int) int {
    return a+b
}

この関数は、入力が整数のため浮動小数点の計算を行う場合は、別の関数を用意する必要があります。例えば、以下のようになります。

func addFloat64(a, b float64) float64 {
    return a+b
}

ジェネリクスがある場合は、この2つの関数をまとめて1つaddという関数で定義することができます。

ジェネリクスがあれば、特定の型に依存しないコードを書くことができるため、同じコードを再利用しやすくなります。

このジェネリクスは、プログラミング言語では一般的な機能でしたが、Go言語ではサポートしていませんでした。

ジェネリクスは、プログラミング言語において一般的な機能であり、コードの柔軟性や安全性を向上させるために広く利用されています。

Go言語でもバージョン1.18からジェネリクスは導入されました。ここでは、Go言語でジェネリクスを使う方法を解説します。

Go言語でジェネリクスが導入されていなかった主な理由は「シンプルさと明快さの追求」です。Go言語の設計思想の1つは、シンプルさを重視することです。ジェネリクスは言語の複雑性を増し、コードの読みやすさを低下させる可能性があります。そのため、ジェネリクスを導入する場合には、この原則とのバランスを考慮する必要がありました。

ジェネリクスの使い方

基本的な使い方

基本的な使い方(1)

簡単なジェネリスクの使い方として、整数型と浮動小数点、文字列をサポートしたAdd関数を定義してみます(文字列の場合は、2つの文字列を連結する操作とする)。

Add関数の定義は以下のようになります。

[T int | float64 | string]の部分が関数が受け入れる型です。関数宣言のこの部分は型パラメータの定義で、この例ではint, float64, stringの3つを受け付けることが宣言されています。

Tを定義してたので、関数の引数や戻り値、内部ではTが入力された型となります。

この関数では、入力に応じたa+bが行われ戻り値として返されます。

func Add[T int | float64 | string](a, b T) T {
	return a + b
}

もう1つ、引数として様々な型を受け付けるPrint関数を定義します。この関数は、内部でfmt.Printlnを呼び出すものです。

anyを用いることで様々な型を受け取ることが可能になります(内部的にはinterface{}と同じ)。

func Print[T any](n T) {
	fmt.Println(n)
}

この2つの関数を利用する場合は以下のようになります(コメント部は実行結果)

このように、引数として渡された型から自動的に、適切な呼び出しがおこなれます。

なお最後の行のPrint[int]のように、型を指定して呼び出すことも可能です。

func main() {
	Print(Add(1, 2))
    // 3
	Print(Add(1.2, 2))
    // 3.2
	Print(Add("a", "b"))
    // ab
	Print[int](10)
    // 10
}

基本的な使い方(2)

typeを使って型に名前をつけておくことも可能です。

下記の例では、numという型をintまたはfloat64として定義し、Sub[T num]と言う形で定義した型を利用して関数を宣言しています。

type num interface {
	int | float64
}

func Sub[T num](a, b T) T {
	return a - b
}

このように、typeを使うことで、コードをシンプルにすることが可能です。

キュー(Queue)を作ってみる

ジェネリクスを使って、いろいろな型を格納可能なキューを実装してみます。

Vectorという型を[]Tとして定義しています。それぞれの関数は以下の処理を行います。

  • Queue関数:キューを初期化し
  • Push関数:キューに値を入力
  • Pop関数:キューから値を取り出し

各関数の記述は、ジェネリクスを使っていることを除けば、Go言語でよく見かける記述なので説明は省略します。

type Vector[T any] []T

func Queue[T any]() *Vector[T] {
	ret := make(Vector[T], 0)
	return &ret
}

func (v *Vector[T]) Push(x T) {
	*v = append(*v, x)
}

func (v *Vector[T]) Pop() T {
	ret := (*v)[0]
	*v = (*v)[1:]
	return ret
}

これを使った例が以下になります。intfloat64はもとより、定義した構造体Pairもキューで取り扱うことができます。

type Pair struct {
	a, b int
}

func main() {
	// int
	{
		q := Queue[int]()
		for i := 0; i < 10; i++ {
			q.Push(i)
		}
		fmt.Println(q)
		for len(*q) != 0 {
			v := q.Pop()
			fmt.Println(v, q)
		}
	}

	// float64
	{
		q := Queue[float64]()
		for i := 0; i < 10; i++ {
			q.Push(float64(i) / 10)
		}
		fmt.Println(q)
		for len(*q) != 0 {
			v := q.Pop()
			fmt.Println(v, q)
		}
	}

    // Pair
	{
		q := Queue[Pair]()
		for i := 0; i < 10; i++ {
			q.Push(Pair{i, i * 10})
		}
		fmt.Println(q)
		for len(*q) != 0 {
			v := q.Pop()
			fmt.Println(v, q)
		}
	}
}

Setを実装してみる

Setをジェネリクスを使って実装してみます。

ここでは、anyではなく、comparableを使いました。comparableを指定すると、型として指定できるのは、==による値の比較ができる型限定になります。

それぞれの関数は以下の処理を行います。

  • Set関数:Setを新規に作成する
  • Add関数:Setに値を追加する
  • Includes関数:値がSetに含まれているかどうかを返す
  • Remove関数:値をSetから削除する

内部では、map[T]boolと、マップ形式で管理しています。

type _Set[T comparable] map[T]bool

func Set[T comparable](xs ...T) _Set[T] {
	s := make(_Set[T])
	for _, xs := range xs {
		s.Add(xs)
	}
	return s
}

func (s _Set[T]) Add(x T) {
	s[x] = true
}

func (s _Set[T]) Includes(x T) bool {
	return s[x]
}

func (s _Set[T]) Remove(x T) {
	delete(s, x)
}

以下が利用例です。

func main() {
	s := Set(1, 2, 3, 3, 4)
	fmt.Println(s)
	s.Add(5)
	fmt.Println(s.Includes(3))
	s.Remove(3)
	fmt.Println(s.Includes(3))
}

上記の例では、Setを整数(int)で利用してますが、ジェネリクスを使っているのでfloat64stringでも利用することが可能です。

このように、1つの関数を様々な型で使えるとことがジェネリクスの利点です

まとめ

ジェネリクスの使い方について解説しました。個人的にはC++でもテンプレートをあまり使わない派なので、ジェネリクスを利用することは少ないと思います。

ただ、ライブラリがジェネリクス対応に切り替わって行けば、かなり便利になるのではないかと感じています。

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

記事URLをコピーしました