Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 15454] ファイルの概要

このコミットは、Go言語の標準ライブラリであるsortパッケージに、構造体を異なるキーでソートする方法を示す新しい例を追加するものです。具体的には、Planetという構造体を定義し、そのnamemassdistanceといったフィールド(キー)に基づいてソートする汎用的な手法を、クロージャと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つのメソッドで構成されます。

  1. Len() int: スライスの要素数を返します。
  2. Swap(i, j int): スライスのi番目とj番目の要素を入れ替えます。
  3. 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ファイルは、構造体のスライスを複数の基準でソートするための汎用的なパターンを実装しています。

  1. Planet構造体: 太陽系の惑星を表す構造体で、name (string), mass (solarMass), distance (au) のフィールドを持ちます。solarMassauはそれぞれfloat64のエイリアスとして定義され、単位の明確化に役立っています。

    type solarMass float64
    type au float64
    
    type Planet struct {
        name     string
        mass     solarMass
        distance au
    }
    
  2. By関数型: Byは、2つの*Planetポインタを引数に取り、boolを返す関数型として定義されています。この関数が、2つのPlanetオブジェクトの比較ロジック(どちらがソート順で前になるか)を定義します。

    type By func(p1, p2 *Planet) bool
    
  3. By型に紐づくSortメソッド: By型にはSortというメソッドが定義されています。このメソッドは[]Planet型のスライスを引数に取り、そのスライスをソートします。 Sortメソッドの内部では、planetSorterというヘルパー構造体が作成され、sort.Sort()関数に渡されます。ここで重要なのは、planetSorterBy型のレシーバ(つまり、ソート基準を定義するクロージャ)を保持している点です。

    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)
    }
    
  4. 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.
    }
    
  5. planetSortersort.Interface実装:

    • Len(): s.planetsの長さを返します。
    • Swap(i, j int): s.planetsi番目と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])
    }
    
  6. 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とクロージャを組み合わせて、構造体のソート基準を動的に変更できるようにする汎用的なパターンを提示している点です。

  1. 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()に渡されます。

  2. 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]) } planetSortersort.InterfaceLessメソッドを実装する際、その比較ロジックをs.byフィールドに格納されているクロージャに委譲しています。これにより、Lessメソッドの具体的な振る舞いは、planetSorterが初期化される際に渡されたクロージャによって決定されます。

  3. 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言語でカスタムソートロジックを柔軟に、かつ再利用可能な形で実装するための非常に効果的な方法を示しています。特に、複数のソート基準が必要な場合に、冗長なコードを避けることができます。

関連リンク

参考にした情報源リンク