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

Go言語のジェネリクスの使い方をQueueとSetの実装を例に解説

Aru

Go言語にも導入されたジェネリクス(Generics)の使い方を、基本的な文法を実例を使って解説します。実例としては、ジェネリクスに対応したQueue(キュー)とSet(集合型)です。ジェネリクスがGo言語に導入されたのは、比較的最近ですので、まだ慣れていない人も多いと思います。本記事では、QueueとSetの実装を通じて、実際にどのように使うのかを詳しく解説します。

ジェネリクスとは

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

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

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

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

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

ジェネリクスを使えば、特定の型に依存しないコードを書くことができんるので、この2つをまとめて1のadd()関数として定義することができます

ジェネリクスは、多くのプログラミング言語に導入されている機能ですが、Go言語ではサポートされていませんでした。このため、引数の型が違う入力に対応するためには、引数の型違いの関数を用意する必要がありました。

interface型を使えば、複数型対応は作れないことはないですが、かなり面倒でした。

Go言語もバージョン1.18よりジェネリクスが導入され、使えるようになりました。ここでは、Go言語でのジェネリクスの記述方法を、サンプルコードを交えて解説します。

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

導入までに、コミュニティや開発者間で結構な議論があったようです。2016年くらいからジェネリクスの提案が始まり、2022年にリリースされたGo 1.18でやっと導入されました。かなり慎重にデザインされ、導入されたことがわかります。

ジェネリクスと整合性が高いのはキューやヒープといった汎用的なアルゴリズムだと思います。ということで、この記事では、QueueSetを実装例に選びました。

ジェネリクスの基本的な使い方

使い方(1)

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

Add関数の例

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

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

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

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

この関数は、T型の変数a, bを受け取り、a+bした結果を返す関数となります。

Print関数の例

別の例として、引数としてどのような型で受け付けるPrint関数を定義してみます。この関数は、内部でfmt.Printlnを呼び出しています(fmt.Println自体が様々な型を入力として受け付けるので、この関数は動作としては意味ないですがサンプルとして理解ください)。

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

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

定義したAdd, Print関数の呼び出し方

この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を使って型に名前をつける例

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)を作ってみる

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

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
}

Queueを呼び出す例(main関数)

これを使った例が以下になります。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をジェネリクスを使って実装してみます。

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)
}

Setを呼び出す例(main関数)

以下が利用例です。

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つの関数を様々な型で使えるとことがジェネリクスの利点です

comparable制約を満たす型であれば、型が構造体であっても問題なく動作します。ここが便利なところです。

まとめ

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

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

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

ABOUT ME
ある/Aru
ある/Aru
IT&機械学習エンジニア/ファイナンシャルプランナー(CFP®)
専門分野は並列処理・画像処理・機械学習・ディープラーニング。プログラミング言語はC, C++, Go, Pythonを中心として色々利用。現在は、Kaggle, 競プロなどをしながら悠々自適に活動中
記事URLをコピーしました