[インデックス 15454] ファイルの概要
このコミットは、Go言語の標準ライブラリであるsort
パッケージに、構造体を異なるキーでソートする方法を示す新しい例を追加するものです。具体的には、Planet
という構造体を定義し、そのname
、mass
、distance
といったフィールド(キー)に基づいてソートする汎用的な手法を、クロージャとsort.Interface
の実装を組み合わせて示しています。
コミット
commit 2c2934eeb539a1b137c48b521d15ff3a77bd53a1
Author: Rob Pike <r@golang.org>
Date: Tue Feb 26 17:17:44 2013 -0800
sort: add an example showing sorting struct by different keys
R=golang-dev, dsymonds
CC=golang-dev
https://golang.org/cl/7376058
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/2c2934eeb539a1b137c48b521d15ff3a77bd53a1
元コミット内容
sort: add an example showing sorting struct by different keys
(和訳)
sort
: 構造体を異なるキーでソートする例を追加
変更の背景
Go言語のsort
パッケージは、スライスをソートするための強力な機能を提供しますが、カスタム型(特に構造体)をソートする際には、sort.Interface
インターフェース(Len()
, Swap(i, j)
, Less(i, j)
の3つのメソッド)を実装する必要があります。しかし、一つの構造体に対して複数のソート基準(例えば、名前順、質量順、距離順など)を適用したい場合、その都度sort.Interface
を実装した新しい型を定義するのは冗長で非効率的です。
このコミットは、このような課題に対するエレガントな解決策として、クロージャ(無名関数)を利用してソート基準を動的に切り替える汎用的なパターンを提示するものです。これにより、開発者は構造体のソートロジックを柔軟に定義し、再利用できるようになります。この例は、Goの標準ライブラリのドキュメントに組み込まれることで、他の開発者が同様の要件に直面した際に役立つ指針となります。
前提知識の解説
Go言語のsort
パッケージ
Go言語のsort
パッケージは、スライスをソートするための機能を提供します。スライスをソートするには、そのスライスの要素がsort.Interface
インターフェースを満たす必要があります。
sort.Interface
は以下の3つのメソッドで構成されます。
Len() int
: スライスの要素数を返します。Swap(i, j int)
: スライスのi
番目とj
番目の要素を入れ替えます。Less(i, j int) bool
: スライスのi
番目の要素がj
番目の要素よりも小さい(ソート順で前になる)場合にtrue
を返します。
これらのメソッドを実装することで、sort.Sort()
関数に任意の型のスライスを渡してソートできるようになります。
クロージャ (Closures)
Go言語におけるクロージャは、関数が自身の外側のスコープにある変数を参照できる機能を持つ無名関数です。クロージャは、その定義された環境(レキシカル環境)を「閉じ込める」ため、関数がその環境外で実行されても、参照している変数の値にアクセスしたり変更したりすることができます。
このコミットの例では、クロージャがソートの比較ロジック(Less
メソッドの内部で使用される)を動的に定義するために利用されています。これにより、ソートの基準を外部から注入することが可能になり、柔軟なソートを実現しています。
関数型 (Function Types)
Go言語では、関数も型として扱うことができます。例えば、func(int, int) bool
は2つのint
型の引数を取り、bool
型の値を返す関数の型を表します。この関数型を変数に代入したり、関数の引数として渡したり、関数の戻り値として返したりすることができます。
このコミットの例では、By
という関数型が定義されており、これがソートの比較ロジックをカプセル化する役割を担っています。
技術的詳細
このコミットで追加されたexample_keys_test.go
ファイルは、構造体のスライスを複数の基準でソートするための汎用的なパターンを実装しています。
-
Planet
構造体: 太陽系の惑星を表す構造体で、name
(string),mass
(solarMass),distance
(au) のフィールドを持ちます。solarMass
とau
はそれぞれfloat64
のエイリアスとして定義され、単位の明確化に役立っています。type solarMass float64 type au float64 type Planet struct { name string mass solarMass distance au }
-
By
関数型:By
は、2つの*Planet
ポインタを引数に取り、bool
を返す関数型として定義されています。この関数が、2つのPlanet
オブジェクトの比較ロジック(どちらがソート順で前になるか)を定義します。type By func(p1, p2 *Planet) bool
-
By
型に紐づくSort
メソッド:By
型にはSort
というメソッドが定義されています。このメソッドは[]Planet
型のスライスを引数に取り、そのスライスをソートします。Sort
メソッドの内部では、planetSorter
というヘルパー構造体が作成され、sort.Sort()
関数に渡されます。ここで重要なのは、planetSorter
がBy
型のレシーバ(つまり、ソート基準を定義するクロージャ)を保持している点です。func (by By) Sort(planets []Planet) { ps := &planetSorter{ planets: planets, by: by, // The Sort method's receiver is the function (closure) that defines the sort order. } sort.Sort(ps) }
-
planetSorter
構造体:planetSorter
は、ソート対象のPlanet
スライスと、ソート基準を定義するBy
関数(クロージャ)を保持するヘルパー構造体です。この構造体がsort.Interface
インターフェース(Len
,Swap
,Less
)を実装します。type planetSorter struct { planets []Planet by func(p1, p2 *Planet) bool // Closure used in the Less method. }
-
planetSorter
のsort.Interface
実装:Len()
:s.planets
の長さを返します。Swap(i, j int)
:s.planets
のi
番目とj
番目の要素を入れ替えます。Less(i, j int)
: ここが最も重要な部分です。s.by(&s.planets[i], &s.planets[j])
を呼び出すことで、planetSorter
が保持するクロージャ(by
フィールド)に実際の比較ロジックを委譲します。これにより、ソートの基準が動的に決定されます。
func (s *planetSorter) Len() int { return len(s.planets) } func (s *planetSorter) Swap(i, j int) { s.planets[i], s.planets[j] = s.planets[j], s.planets[i] } func (s *planetSorter) Less(i, j int) bool { return s.by(&s.planets[i], &s.planets[j]) }
-
Example_sortKeys
関数: この関数は、上記のメカニズムを使って実際にPlanet
スライスをソートする例を示しています。name
,mass
,distance
という3つのクロージャを定義しています。これらはそれぞれ、Planet
構造体の異なるフィールドに基づいて比較を行うBy
型の関数です。decreasingDistance
というクロージャは、distance
クロージャの結果を反転させることで、降順ソートを実現しています。- 各クロージャを
By()
でラップし、そのSort()
メソッドを呼び出すことで、planets
スライスがそれぞれの基準でソートされ、結果が出力されます。
func Example_sortKeys() { // Closures that order the Planet structure. name := func(p1, p2 *Planet) bool { return p1.name < p2.name } mass := func(p1, p2 *Planet) bool { return p1.mass < p2.mass } distance := func(p1, p2 *Planet) bool { return p1.distance < p2.distance } decreasingDistance := func(p1, p2 *Planet) bool { return !distance(p1, p2) } // Sort the planets by the various criteria. By(name).Sort(planets) fmt.Println("By name:", planets) By(mass).Sort(planets) fmt.Println("By mass:", planets) By(distance).Sort(planets) fmt.Println("By distance:", planets) By(decreasingDistance).Sort(planets) fmt.Println("By decreasing distance:", planets) // Output: By name: [{Earth 1 1} {Mars 0.107 1.5} {Mercury 0.055 0.4} {Venus 0.815 0.7}] // By mass: [{Mercury 0.055 0.4} {Mars 0.107 1.5} {Venus 0.815 0.7} {Earth 1 1}] // By distance: [{Mercury 0.055 0.4} {Venus 0.815 0.7} {Earth 1 1} {Mars 0.107 1.5}] // By decreasing distance: [{Mars 0.107 1.5} {Earth 1 1} {Venus 0.815 0.7} {Mercury 0.055 0.4}] }
このパターンは、Go言語における「関数型オプション」や「戦略パターン」の一種と見なすことができ、柔軟性と再利用性を高めるための良い例となっています。
コアとなるコードの変更箇所
追加されたファイル: src/pkg/sort/example_keys_test.go
このファイル全体が新しい例として追加されています。
--- /dev/null
+++ b/src/pkg/sort/example_keys_test.go
@@ -0,0 +1,96 @@
+// Copyright 2013 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package sort_test
+
+import (
+ "fmt"
+ "sort"
+)
+
+// A couple of type definitions to make the units clear.
+type solarMass float64
+type au float64
+
+// A Planet defines the properties of a solar system object.
+type Planet struct {
+ name string
+ mass solarMass
+ distance au
+}
+
+// By is the type of a "less" function that defines the ordering of its Planet arguments.
+type By func(p1, p2 *Planet) bool
+
+// Sort is a method on the function type, By, that sorts the argument slice according to the function.
+func (by By) Sort(planets []Planet) {
+ ps := &planetSorter{
+ planets: planets,
+ by: by, // The Sort method's receiver is the function (closure) that defines the sort order.
+ }
+ sort.Sort(ps)
+}
+
+// planetSorter joins a By function and a slice of Planets to be sorted.
+type planetSorter struct {
+ planets []Planet
+ by func(p1, p2 *Planet) bool // Closure used in the Less method.
+}
+
+// Len is part of sort.Interface.
+func (s *planetSorter) Len() int {
+ return len(s.planets)
+}
+
+// Swap is part of sort.Interface.
+func (s *planetSorter) Swap(i, j int) {
+ s.planets[i], s.planets[j] = s.planets[j], s.planets[i]
+}
+
+// Less is part of sort.Interface. It is implemented by calling the "by" closure in the sorter.
+func (s *planetSorter) Less(i, j int) bool {
+ return s.by(&s.planets[i], &s.planets[j])
+}
+
+var planets = []Planet{
+ {"Mercury", 0.055, 0.4},
+ {"Venus", 0.815, 0.7},
+ {"Earth", 1.0, 1.0},
+ {"Mars", 0.107, 1.5},
+}
+
+// ExampleSortKeys demonstrates a technique for sorting a struct type using programmable sort criteria.
+func Example_sortKeys() {
+ // Closures that order the Planet structure.
+ name := func(p1, p2 *Planet) bool {
+ return p1.name < p2.name
+ }
+ mass := func(p1, p2 *Planet) bool {
+ return p1.mass < p2.mass
+ }
+ distance := func(p1, p2 *Planet) bool {
+ return p1.distance < p2.distance
+ }
+ decreasingDistance := func(p1, p2 *Planet) bool {
+ return !distance(p1, p2)
+ }
+
+ // Sort the planets by the various criteria.
+ By(name).Sort(planets)
+ fmt.Println("By name:", planets)
+
+ By(mass).Sort(planets)
+ fmt.Println("By mass:", planets)
+
+ By(distance).Sort(planets)
+ fmt.Println("By distance:", planets)
+
+ By(decreasingDistance).Sort(planets)
+ fmt.Println("By decreasing distance:", planets)
+
+ // Output: By name: [{Earth 1 1} {Mars 0.107 1.5} {Mercury 0.055 0.4} {Venus 0.815 0.7}]
+ // By mass: [{Mercury 0.055 0.4} {Mars 0.107 1.5} {Venus 0.815 0.7} {Earth 1 1}]
+ // By distance: [{Mercury 0.055 0.4} {Venus 0.815 0.7} {Earth 1 1} {Mars 0.107 1.5}]
+ // By decreasing distance: [{Mars 0.107 1.5} {Earth 1 1} {Venus 0.815 0.7} {Mercury 0.055 0.4}]
+}
コアとなるコードの解説
このコミットの核心は、Goのsort.Interface
とクロージャを組み合わせて、構造体のソート基準を動的に変更できるようにする汎用的なパターンを提示している点です。
-
By
関数型とSort
メソッド:type By func(p1, p2 *Planet) bool
この行は、2つのPlanet
ポインタを受け取り、bool
を返す関数をBy
という新しい型として定義しています。この関数が、ソートにおける「より小さい」という比較ロジックをカプセル化します。func (by By) Sort(planets []Planet)
By
型にSort
というメソッドが追加されています。これは、By
型の値(つまり、特定の比較ロジックを持つ関数)に対して直接ソート操作を呼び出せるようにします。このメソッド内で、実際のソート処理を行うplanetSorter
が初期化され、sort.Sort()
に渡されます。 -
planetSorter
構造体とLess
メソッド:type planetSorter struct { planets []Planet; by func(p1, p2 *Planet) bool }
planetSorter
は、ソート対象のスライスと、ソート基準となるBy
関数(クロージャ)を保持するための内部的なヘルパー構造体です。func (s *planetSorter) Less(i, j int) bool { return s.by(&s.planets[i], &s.planets[j]) }
planetSorter
がsort.Interface
のLess
メソッドを実装する際、その比較ロジックをs.by
フィールドに格納されているクロージャに委譲しています。これにより、Less
メソッドの具体的な振る舞いは、planetSorter
が初期化される際に渡されたクロージャによって決定されます。 -
Example_sortKeys
関数におけるクロージャの活用:name := func(p1, p2 *Planet) bool { return p1.name < p2.name }
mass := func(p1, p2 *Planet) bool { return p1.mass < p2.mass }
distance := func(p1, p2 *Planet) bool { return p1.distance < p2.distance }
これらの行では、Planet
構造体の異なるフィールド(name
,mass
,distance
)に基づいて比較を行う3つのクロージャが定義されています。これらはすべてBy
型に適合します。By(name).Sort(planets)
このように、定義したクロージャをBy
型にキャストし、そのSort
メソッドを呼び出すだけで、planets
スライスをそれぞれの基準でソートできます。decreasingDistance
の例では、既存のdistance
クロージャの結果を反転させることで、降順ソートを簡単に実現しています。
このパターンは、Go言語でカスタムソートロジックを柔軟に、かつ再利用可能な形で実装するための非常に効果的な方法を示しています。特に、複数のソート基準が必要な場合に、冗長なコードを避けることができます。
関連リンク
- Go言語
sort
パッケージ公式ドキュメント: https://pkg.go.dev/sort - Go言語のクロージャに関する解説 (Go by Example): https://gobyexample.com/closures
参考にした情報源リンク
- Go言語
sort
パッケージのソースコード (GitHub): https://github.com/golang/go/tree/master/src/sort - Go言語のコミット履歴 (GitHub): https://github.com/golang/go/commits/master
- Go Code Review Comments - Example functions: https://go.dev/doc/effective_go#example_functions
- Go言語の関数型プログラミングパターンに関する記事 (例: Strategy Pattern in Go): https://refactoring.guru/design-patterns/strategy/go/example (一般的なデザインパターンとしての参考)