フューチャー技術ブログ

Go 1.21 リリース連載 待望の組み込み関数min/maxと新パッケージcmpの挙動確認

はじめに

Goを書き始めてから10年ほど経ちますが、最近は全然書けていない谷村です。久々にGoの新しいところに触れていこうと思いますのでお付き合いください。

本記事では、min/max組み込み関数と、新設されたcmpパッケージについて触れます。「Goには何故min/max関数が無いんだ」と嘆いた数々のGo初学者たちが救われるかもしれません。

※本記事のコードリーディングおよび動作確認は、go1.21rc3で実施しました。

min/max function

Go 1.21 Relase Notesでは以下のように述べられています。

The new functions min and max compute the smallest (or largest, for max) value of a fixed number of given arguments. See the language spec for details.

要は各リポジトリで以下のように書いていたものが、組み込み関数化された、と思って良さそうです。

func min(x, y int) int {
if x < y {
return x
}
return y
}
func min(nums []int) int {
min := nums[0]
for _, n := range nums {
if n < min {
min = n
}
}
return min
}

仕様詳細

Release NotesのDetailsに、いくつかの仕様詳細が記載されています。直感的ではない仕様や、記載がないけど気になる仕様もあるので、いくつか抜粋して紹介します。

受け入れ可能な型の組み合わせ

The same type rules as for operators apply: for ordered arguments x and y, min(x, y) is valid if x + y is valid, and the type of min(x, y) is the type of x + y (and similarly for max). If all arguments are constant, the result is constant.

受け入れ可能な変数型は複数あり、内部でGenericsを用いてることが予想されます。
受け入れ可能な型の組み合わせは、足し算(プラスオペレーター)が正しく実行されることが必要十分条件とのことです。min/maxで返される型も足し算結果と同様ということです。

スライスの受入れ不可

_ = min(s...)               // invalid: slice arguments are not permitted

と記載があり、スライスは受け入れ不可とのことです。
Release Noteに記載を見つけられないのですが、今回から追加されたらしいslices pakcegeに、Min/Maxが存在するからだと思われます。以下、利用サンプルです。

package main

import (
"fmt"
"slices"
)

func main() {
nums := []int{3, 4, 9, 4, 2, 10}
fmt.Println(slices.Min(nums))
}

なお、公式サンプルにも記載されていますが、min/maxは要素3つ以上でも受け入れ可能です。

文字列の受け入れ

t := max("", "foo", "bar")  // t == "foo" (string kind)

とあり、文字列も受け入れ可能とのことです。
これだけでは仕様の詳細がわからないため、手元でも動かしてみました。

package main

import (
"fmt"
)

func main() {
t1 := max("", "foo", "bar")
t2 := min("", "foo", "bar")
fmt.Println(t1)
fmt.Println(t2)
}

実行結果は、以下の通り。

foo
(空文字)

アルファベット順に並べたときに後ろに来るものがmax、空文字は問答無用で最小扱いのようです。

冒頭のような独自関数との競合

色んな言語を渡り歩いていると、同一の変数名と関数名や、引数の異なる同名関数などの共存可否を忘れてしまいます。(私がそうでした)
既存コードにはmin/maxの文字が溢れていると思われますが、golang1.20以前から1.21以降へのバージョンアップでこのへんは懸念になりうるのでしょうか。

以下のようなコードでも動くので、既存コードへの影響はあまり考えなくても良いと思われます。
ただし、別pakcageに存在する独自のminと組み込みのminのどちらが優先されるかなど、状況によっては気にかける必要があるかもしれません。

package main

import (
"fmt"
)

func main() {
max := max("", "foo", "bar") // 組み込み関数と同名変数の利用OK
min := min("", "foo", "bar")
fmt.Println(max)
fmt.Println(min)

fmt.Println(min(5, 3)) // 同package内の場合、独自関数が優先される
}

func min(x, y int) int {
fmt.Println("my function")
if x < y {
return x
}
return y
}

なぜライブラリではなく組み込みなのか

ポリシーによって唯一の正解はなさそうですが、この辺のコメントが決定打だったようです。
理由のうちの一つの「NaNの扱いの統一性」は、なるほど、と思いました。
https://github.com/golang/go/issues/59488#issuecomment-1548505279

実装の中身

最初は以下の builtin.go かなと思ったんですが、builtinのGoDocに

The items documented here are not actually in package builtin but their descriptions here allow godoc to present documentation for the language’s special identifiers.

と記載があり、実際にはビルドされないようです。実態はruntimeを参照しているものと思われます。

src/builtin/builtin.go
// The max built-in function returns the largest value of a fixed number of
// arguments of [cmp.Ordered] types. There must be at least one argument.
// If T is a floating-point type and any of the arguments are NaNs,
// max will return NaN.
func max[T cmp.Ordered](x T, y ...T) T

// The min built-in function returns the smallest value of a fixed number of
// arguments of [cmp.Ordered] types. There must be at least one argument.
// If T is a floating-point type and any of the arguments are NaNs,
// min will return NaN.
func min[T cmp.Ordered](x T, y ...T) T

cmp package

golangでcmpといえば、go-cmpですよね!

go-cmpはgoogle配下に存在していたので公式みたいなものだと思ってますが、Go本体に組み込まれたのが新しい、、、と思ってたのですが、全然違いました。

The new cmp package defines the type constraint Ordered and two new generic functions Less and Compare that are useful with ordered types.

Release Notesの説明では、上記のように記載があり、2つの何かを比較して返してくれるジェネリクス関数のようです。
ただ、これだけだとサッパリわかりません。godocを読んでみましょう。

func Compare[T Ordered](x, y T) int は以下のようなreturnをするそうです。

-1 if x is less than y,
0 if x equals y,
+1 if x is greater than y.

func Less[T Ordered](x, y T) bool は以下のように、「第一引数が第二引数未満かどうか」をboolで返すようです。つまり、同一値であればfalseが返りそうです。

whether x is less than y

使える型が気になるところですが、以下のように単一の値であれば何でも使える仕様です。

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

structの比較定義を作ってそれに従って返してくれても良い気もしましたが、従来よりsort packageがありますし、単なる比較レベルなら個別実装で十分でしょう。

挙動確認

以下3観点の確認コードを用意しました。

import (
"cmp"
"fmt"
)

func main() {
intx := int(3)
inty := int(4)
intz := int(3)

float64y := float64(4.0)

// 主要ケース確認
fmt.Println(cmp.Compare(intx, inty))
fmt.Println(cmp.Less(intx, inty))
// 同一値の確認
fmt.Println(cmp.Compare(intx, intz))
fmt.Println(cmp.Less(intx, intz))
// 型違いの比較
fmt.Println(cmp.Compare(intx, float64y))
fmt.Println(cmp.Less(intx, float64y))
}

が、まずは型違いが以下コンパイルエラーでダメ。

./cmp.go:22:32: type float64 of float64y does not match inferred type int for T
./cmp.go:23:29: type float64 of float64y does not match inferred type int for T

気を取り直して型違い比較の箇所を外して実行した結果は以下。

-1 // xがyより小さいので-1が返る
true // xがyより小さいのでtrueが返る
0 // xとzが同一値なので0が返る
false // xがz「未満ではない」のでfalseが返る

なんとも面白みもありませんが、読み取った仕様通りの挙動を確認できました。

正直、 これらが無くても、自前実装で良いと思ったんですがどうなんでしょう…
実際の実装も以下のレベルですし。

src/cmp/cmp.go
// Compare returns
//
// -1 if x is less than y,
// 0 if x equals y,
// +1 if x is greater than y.
//
// For floating-point types, a NaN is considered less than any non-NaN,
// a NaN is considered equal to a NaN, and -0.0 is equal to 0.0.
func Compare[T Ordered](x, y T) int {
xNaN := isNaN(x)
yNaN := isNaN(y)
if xNaN && yNaN {
return 0
}
if xNaN || x < y {
return -1
}
if yNaN || x > y {
return +1
}
return 0
}
src/cmp/cmp.go
// Less reports whether x is less than y.
// For floating-point types, a NaN is considered less than any non-NaN,
// and -0.0 is not less than (is equal to) 0.0.
func Less[T Ordered](x, y T) bool {
return (isNaN(x) && !isNaN(y)) || x < y
}

既存影響

広く使われている go-cmp のpackage名がcmpなので、既存コードと競合しそうだと思いました。(詳細未確認)

go-cmp を使ってるのは主にテストコードだと思われるので本番挙動には影響ないと思いますが、go versionを上げてテストが落ちたらココを疑っても良いかもしれません。(go-cmpに異なるaliasを付けてimportすることになりそうです…)

まとめ

min/maxも、slicesも、cmp.Compare/cmp.Lessも、かゆいところに手が届くかもしれない関数だと感じました。

sortやtime formatで当初はバリバリに尖っていたGoも、歳を重ねて丸くなってきたように感じます。初学者にとってわかりやすく、熟練者のタイプ数も減るのであればそれ以上のことは無いのかもしれません。

次回はオチラルさんのmaps記事です。

アイキャッチはMichael SchwarzenbergerによるPixabayからの画像です。