oinume journal

Scratchpad of what I learned

Go1.17beta1でGenericsに触れてみた

Go1.17beta1がダウンロードできるようになったので、Generics(Type Parameters)でStackを書いて軽く遊んだメモ。

Generics (Type Parameters)について

最新の仕様のProposalは以下を見ると良い。

実際にどんな感じのコードになるのかは、GitHubのdev.typeparams branchのコードを見ると良さそう。

Go1.17beta1のダウンロード

普段使っているgoコマンドから以下のようにGo1.17beta1をダウンロードできる。

$ go get golang.org/dl/go1.17beta1
$ go1.17beta1 download

これで ~/sdk/go1.17beta1 にインストールされる。

FYI: Goは標準の機能で複数のバージョンをインストールできる。より詳しくは以下のドキュメントを読むと良い。

Genericsのサンプルコード

Go1.17beta1に付属している以下のディレクトリに結構ある。

$ ls $(go1.17beta1 env GOROOT)/test/typeparam
absdiff.go      fact.go         issue45738.go   min.go          slices.go       tparam1.go
adder.go        graph.go        list.go         ordered.go      smallest.go     typelist.go
append.go       importtest.go   list2.go        orderedmap.go   smoketest.go    value.go
chans.go        index.go        lockable.go     pair.go         stringable.go
combine.go      interfacearg.go map.go          pragma.go       stringer.go
cons.go         issue45547.go   maps.go         sets.go         struct.go
double.go       issue45722.go   metrics.go      settable.go     sum.go

GenericsでStackを書いてみる

こんな感じになった。現時点ではType Parametersの型をexportすることができないっぽいので、stackと小文字で定義してmainパッケージに書いている。

// run -gcflags=-G=3
package main

import "fmt"

var (
    ErrEmptyStack = fmt.Errorf("stack is empty")
)

type stack[T any] struct {
    data     []T
    capacity int
}

func newStack[T any](capacity int) *stack[T] {
    if capacity <= 0 {
        panic("must be 'capacity' > 0")
    }
    data := make([]T, 0, capacity)
    return &stack[T]{data: data, capacity: capacity}
}

func (s *stack[T]) Push(v T) {
    s.data = append(s.data, v)
}

func (s *stack[T]) Pop() (T, error) {
    if s.Size() == 0 {
        var zero T
        return zero, ErrEmptyStack
    }
    v := s.data[len(s.data)-1]
    s.data = s.data[:len(s.data)-1]
    return v, nil
}

func (s *stack[T]) Peek() (T, error) {
    if s.Size() == 0 {
        var zero T
        return zero, ErrEmptyStack
    }
    return s.data[len(s.data)-1], nil
}

func (s *stack[T]) Size() int {
    return len(s.data)
}

func main() {
    s := newStack[int](10)
    s.Push(1)
    s.Push(2)
    s.Push(3)
    fmt.Printf("s = %+v\n", s)

    _, err := s.Pop()
    if err != nil {
        panic(err)
    }
    fmt.Printf("s = %+v\n", s)
}

コード書いてて少し悩んだのは、func (s *stack[T]) Pop() (T, error) のようなメソッドでerrorを返す時にT型のゼロ値をどうやって返すのか、という点だった。どうやら以下のように var zero T を定義してそれを返すのが良いらしい。return nil, ErrEmptyStackという風に書くと、Tはポインタ型ではないのでコンパイルエラーになる。

func (s *stack[T]) Pop() (T, error) {
    if s.Size() == 0 {
        var zero T
        return zero, ErrEmptyStack
    }
    v := s.data[len(s.data)-1]
    s.data = s.data[:len(s.data)-1]
    return v, nil
}

実行

main関数があれば普通にgo1.17beta1 runコマンドで実行できる。-gcflags=-G=3を付けないとgenericsのコードはコンパイルできないので注意。

$ go1.17beta1 run -gcflags=-G=3 ./typeparam.go
s = &{data:[1 2 3] capacity:10}
s = &{data:[1 2] capacity:10}

感想

Type Parametersの機能が入るリリースは2022年の2月のGo 1.18だと思うけど、現時点でもモリモリ実装されているのでとても楽しみである。