😀

10分de技術知 golangのGenerics - slicesパッケージの関数定義がわかるようになるぐらいの技術知まとめ

2023/11/07に公開

golangのGenerics

概要

GolangのGenericsについて10分で読めるくらいのまとめ。

本記事の目的は2つあります。

1つは「標準ライブラリslicesパッケージの関数定義がわかるようになるぐらいの技術知まとめ」ぐらいの、Genericsに関する知識を得ること(・・・というか、著者はそのために勉強した)。

例えば、slicesパッケージのContains関数の定義が下記です。初見だと意味が分からず。元々、私はJavaのGenericsやC++のテンプレートに関する知識はあるので、勉強しなくてもなんとなくわかるだろ、ぐらいに思っていましたが・・・(特に、comparableやチルダ~の意味がよくわからない)。

func Contains[S ~[]E, E comparable](s S, v E) bool

2つ目はGoのGenericsに関連する用語の意味を理解すること。type parameterやtype constraintなどなど。

Genericsとは

Genericsは、Go1.18から導入された新しい言語仕様。

関数や構造体に型パラメータ(type parameter)を定義することができる。関数や構造体の利用者は、型パラメータに対して具体的な型を指定し、その関数や構造体を利用する。

Genericsのメリットは、関数や構造体を、複数の型で利用できること。

具体例

下記の2つの関数(AddNumberInt、AddNumberFloat32)は中身は同じ。引数、返り値の型だけが異なる。

package main

import "fmt"

func main() {
  fmt.Println(AddNumberInt(1, 2), AddNumberFloat32(1.0, 2.0))
}

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

func AddNumberFloat32(a, b float32) float32 {
  return a + b
}

上記2つの関数を、1つの関数にしてみる。すると下記のようになる。

package main

import "fmt"

func main() {
  fmt.Println(
    AddNumber[int](1, 2),
    AddNumber[float32](float32(1.0), float32(2.0)),
  )
}

func AddNumber[N int | float32](a, b N) N {
  return a + b
}

型パラメータは[N int | float32]。AddNumber関数は複数の型、intとfloat32で利用できる。

2つあった関数を1つにまとめることができた。冗長なソースコードが減った。これがGenericsのメリット。

Genericsの基本

Genericsの記法

型パラメータ(type parameter)

型パラメータ(type parameter)は、関数(function)と型(type)に対して付与できます。型パラメータは、カンマ区切りで複数指定することができます。

型引数(type argument) と 型制約 (type constraint)

型パラメータは、型引数(type argument)と型制約(type constraint)で構成されます。

関数

func 関数名[型引数 型制約,...] (引数) 返り値 {
  関数の中身
}
// 例
func SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V {
    var s V
    for _, v := range m {
        s += v
    }
    return s
}
// `[K comparable, V int64 | float64]` カンマ区切りで複数の型パラメータがある例。
// `K comparable`が1つ目の型パラメータ
// `K`が型引数、`comparable`が型制約
// `V int64 | float64`が2つ目の型パラメータ
// `V`が型引数、`int64 | float64`が型制約

構造体

// 構造体
type 構造体名[型引数 型制約,...] struct {}
// メソッド
func (t 構造体名[型引数]) メソッド名(引数) 返り値 {
  メソッドの中身
}
// 例
type Computer[K comparable, V int64 | float64] struct {
    m map[K]V
}

func (t *Computer[K, V]) Set(k K, v V) {
    t.m[k] = v
}

func (t *Computer[K, V]) Sum() V {
    var s V
    for _, v := range t.m {
        s += v
    }
    return s
}

インターフェース

// インターフェース
type インターフェース名[型引数 型制約,...] interface {
}
// 例
type ComputerInterface[K comparable, V int64 | float64] interface {
    Set(k K, v V)
    Sum() V
}

型定義

こんなこともできる。しかしどんな意味があるのだろうか?

// 例
type A[V comparable] int64
type B A[string]

型制約

型制約は、関数の利用側がその型パラメータにおいて利用可能な型。

例えば、下記の関数の場合

func FuncA[K comparable, V int64 | float64](a1 K, a2 V)

型パラメータ K comparableにおいて、comparableが型制約。関数の利用側は、2つの演算子==!=で比較できる型を、Kに指定できる。

型パラメータ V int64 | float64において、int64 | float64が型制約。関数の利用側は、int64またはfloat64のどちらかの型を、Vに指定できる。

関数の利用側の記述例。

func main5() {
        FuncA(1, int64(1))
        FuncA("hoge", float64(1))
        // Kに指定された構造体も==と!=で比較的るため、comparableな型として指定可能
        FuncA(struct{}{}, float64(1))
        // 下記はNG Vにはfloat32を指定できない
        // FuncA(1, float32(1))
        // 下記はNG Kに指定されたsliceやmapはcomparableな型ではない
        // FuncA([]int64{1, 2, 3}, float64(1))
        // FuncA(map[string]string{"hoge": "fuga"}, float64(1))
}

チルダ(~)が付いた型制約

型制約にチルダがついていることがある。チルダの意味は下記。

  • 型制約~Tは、関数の利用側は、Tを基礎型(underlying type)とする全ての型を、型パラメータとして指定できる。

例えば、下記の関数。

func f2[N ~int64](n N) {}

f2の利用側は、int64を基礎型とする全ての型を、型パラメータとして指定できる。
ゆえに、f2の利用側の記述例は下記のようになる。

package main

func main() {
    f1(int64(1))
    f2(int64(1))
    f1(X(1)) // コンパイルエラーとなる
    f2(X(1))
}

// Xという型を宣言する。
// この時、X の基礎型は int64 となる。
type X int64

// f1 は、int64 型のみを許容する。
// f1 は、int64を基礎型として持つ X を許容しない。
func f1[N int64](n N)  {}

// f2 は、int64 型を基礎型として持つ全ての型を許容する。
// よって、X を許容する。
func f2[N ~int64](n N) {}

Goの言語仕様として、1つの型は基礎型を1つの基礎型を持つ、というものがある。
その型の基礎型が何か?は言語仕様に書かれているが、詳細はかなり長くなりそうなので、ここでの説明は割愛する。知りたい方は言語仕様を。

型制約(type constraint)の宣言

型制約をinterfaceとして宣言できる。宣言された型制約は、複数箇所で利用されることができる。

// 型制約 MyNumber の宣言
type MyNumber interface {
    int | float32
}

// 宣言された型制約 MyNumber を利用する
func AddNumber[N MyNumber](a, b N) N {
    return a + b
}

型指定の省略

型パラメータが付与された関数の利用側のコードでは、その関数の型パラメータが推論可能な場合、型の指定を省略することができる。

package main

import "fmt"

func main() {
  // 型パラメータが付与された関数AddNumberの利用側のコード
  fmt.Println(
    // AddNumber[int](1, 2),
    // AddNumber[float32](float32(1.0), float32(2.0)),
    AddNumber(1, 2), // [int]を省略可
    AddNumber(float32(1.0), float32(2.0)), // [float32]を省略可
  )
}

// AddNumber は型パラメータが付与された関数
func AddNumber[N int | float32](a, b N) N {
  return a + b
}

ビルトインな型制約 comparable

comparableは「2つの演算子==!=で比較できる型」という意味。
これはビルトインな型制約であり、言語仕様に記載されている。

あらゆるプリミティブ型(intやfloatや文字列型、etc...)はcomparable。構造体もcomparableである。

comparableとは何か?の説明は、これまた長くなりそうなので割愛する。多分この辺に書いてある。

slicesパッケージの型パラメータの意味

ここまで説明した知識があれば、標準ライブラリ slices パッケージの意味を理解できる(・・・と思います。はい汗)。

genericsもう怖くない。

cmpパッケージ

型制約 Ordered は下記のように宣言されている。この宣言の意味は、「Orderedは< <= >= >を適用可能な型」ということ。日本語で要約すると「順序がある型」という意味。

type Ordered interface {
  ~int | ~int8 | ~int16 | ~int32 | ~int64 |
    ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
    ~float32 | ~float64 |
    ~string
}

型制約Orderedは、チルダ~が用いられているため、基礎型から派生する全ての型を許容する。チルダ~によって、より多くの型で利用可能な型制約を実現できるんですなぁ。

// 順序がある2つの変数を比較する関数
func Compare[T Ordered](x, y T) int

slicesパッケージ

func Contains[S ~[]E, E comparable](s S, v E) bool

この関数は、配列 s の中に要素 v が含まれるかどうかを判定する関数。

Eはcomparableな型。
Sは[]Eを基礎型に持つ型。

Discussion