コンテンツにスキップ

Go/comparable

出典: フリー教科書『ウィキブックス(Wikibooks)』
< Go

comparableはGoにおいて事前宣言された識別子で、Go 1.18で導入されたタイプ制約(type constraint)です。==!=演算子による比較が可能な全ての型を表す型制約として機能します。

基本的な特徴

[編集]
  • 型制約(type constraint)として機能
  • 等値比較(==!=)が可能な型セットを表す
  • 主にジェネリクスで使用される
  • Go 1.18以降で使用可能
  • 以下の型が含まれる:
    • ブール型
    • 数値型(整数、浮動小数点、複素数)
    • 文字列型
    • ポインタ型
    • チャネル型
    • インターフェース型(型セットに非比較可能な型を含まない場合)
    • 比較可能な型のみを含む配列型
    • 全てのフィールドが比較可能な構造体型

他のキーワードとの組み合わせ

[編集]

ジェネリック関数での使用

[編集]
// 等値比較が必要なジェネリック関数
func Equal[T comparable](a, b T) bool {
    return a == b
}

// 使用例
Equal(10, 10)      // true
Equal("a", "b")    // false
Equal(1.5, 1.5)    // true

ジェネリック型定義での使用

[編集]
// comparableを制約として持つジェネリック型
type Pair[T comparable] struct {
    First, Second T
}

// メソッド
func (p Pair[T]) Equals(other Pair[T]) bool {
    return p.First == other.First && p.Second == other.Second
}

マップのキー型制約

[編集]
// キーがcomparable制約を満たす汎用的なマップラッパー
type SafeMap[K comparable, V any] struct {
    mu sync.RWMutex
    data map[K]V
}

func (m *SafeMap[K, V]) Get(key K) (V, bool) {
    m.mu.RLock()
    defer m.mu.RUnlock()
    val, ok := m.data[key]
    return val, ok
}

カスタム型制約との組み合わせ

[編集]
// comparable制約に他の制約を追加
type ComparableNumber interface {
    comparable
    int | int8 | int16 | int32 | int64 | float32 | float64
}

// 比較可能な数値型に対してのみ機能する汎用関数
func Max[T ComparableNumber](a, b T) T {
    if a > b {
        return a
    }
    return b
}

インターフェースとの組み合わせ

[編集]
// 通常のインターフェースとcomparableの組み合わせ
type Identifier interface {
    comparable
    ID() string
}

// 使用例
func FindByID[T Identifier](items []T, target T) int {
    for i, item := range items {
        if item == target {
            return i
        }
    }
    return -1
}

anyとcomparableの違い

[編集]
// any (interface{}) - 全ての型を受け付けるが比較できない可能性がある
func ProcessAny[T any](val T) {
    // val == val  // コンパイルエラーの可能性あり
}

// comparable - 比較可能な型のみ受け付ける
func ProcessComparable[T comparable](val T) {
    _ = val == val  // 常に安全
}

ユースケース

[編集]
  1. ジェネリックなコンテナ型
    • マップのキーやセットの要素として使用可能な型を制約
       type Set[T comparable] struct {
           items map[T]struct{}
       }
       
       func (s *Set[T]) Add(item T) {
           s.items[item] = struct{}{}
       }
       
       func (s *Set[T]) Contains(item T) bool {
           _, ok := s.items[item]
           return ok
       }
    
  2. キャッシュの実装
    • キーとして比較可能な型を要求
       type Cache[K comparable, V any] struct {
           data map[K]V
           expiry map[K]time.Time
       }
       
       func (c *Cache[K, V]) Get(key K) (V, bool) {
           if c.isExpired(key) {
               c.Delete(key)
               var zero V
               return zero, false
           }
           v, ok := c.data[key]
           return v, ok
       }
    
  3. ルックアップテーブル
    • 高速ルックアップのための比較可能なキーを使用
       func CreateLookupTable[K comparable, V any](pairs [][2]any) map[K]V {
           result := make(map[K]V)
           for _, pair := range pairs {
               key, ok1 := pair[0].(K)
               value, ok2 := pair[1].(V)
               if ok1 && ok2 {
                   result[key] = value
               }
           }
           return result
       }
    
  4. 重複検出
    • スライス内の重複要素を検出
       func FindDuplicates[T comparable](items []T) []T {
           seen := make(map[T]bool)
           var duplicates []T
           
           for _, item := range items {
               if seen[item] {
                   duplicates = append(duplicates, item)
               } else {
                   seen[item] = true
               }
           }
           
           return duplicates
       }
    
  5. メモ化(Memoization)
    • 関数呼び出し結果のキャッシュ
       func Memoize[T comparable, R any](fn func(T) R) func(T) R {
           cache := make(map[T]R)
           
           return func(arg T) R {
               if result, found := cache[arg]; found {
                   return result
               }
               
               result := fn(arg)
               cache[arg] = result
               return result
           }
       }
    
  6. インメモリデータベース
    • キーとして比較可能な型を使用
       type Table[K comparable, V any] struct {
           rows map[K]V
       }
       
       func (t *Table[K, V]) Insert(key K, value V) {
           t.rows[key] = value
       }
       
       func (t *Table[K, V]) Select(key K) (V, bool) {
           value, exists := t.rows[key]
           return value, exists
       }
    
  7. ジェネリックなアルゴリズム
    • 比較を要する汎用アルゴリズム
       // 線形探索
       func LinearSearch[T comparable](items []T, target T) int {
           for i, item := range items {
               if item == target {
                   return i
               }
           }
           return -1
       }
       
       // 集合演算
       func Union[T comparable](a, b []T) []T {
           seen := make(map[T]bool)
           var result []T
           
           for _, item := range a {
               if !seen[item] {
                   seen[item] = true
                   result = append(result, item)
               }
           }
           
           for _, item := range b {
               if !seen[item] {
                   seen[item] = true
                   result = append(result, item)
               }
           }
           
           return result
       }
    
  8. 構造データの比較
    • コンポジット型の比較
       type Point[T comparable] struct {
           X, Y T
       }
       
       func ArePointsEqual[T comparable](p1, p2 Point[T]) bool {
           return p1 == p2  // 構造体の全フィールドがcomparableなので安全
       }
    
  9. グラフデータ構造
    • ノードやエッジの識別に使用
       type Graph[N comparable] struct {
           edges map[N][]N
       }
       
       func (g *Graph[N]) AddEdge(from, to N) {
           g.edges[from] = append(g.edges[from], to)
       }
       
       func (g *Graph[N]) GetNeighbors(node N) []N {
           return g.edges[node]
       }
    
  10. 型に安全なイコールチェック
    • ジェネリック型の厳密な比較
        func IsEqual[T comparable](lhs, rhs T) bool {
            return lhs == rhs
        }
        
        // 型に安全な使用
        IsEqual(1, 1)        // OK
        IsEqual("a", "a")    // OK
        // IsEqual(1, "1")   // コンパイルエラー - 型が不一致
    
  11. 並列実行制御
    • 比較可能なジョブIDなどを使用したタスク管理
        type JobManager[ID comparable, Result any] struct {
            results map[ID]Result
            running map[ID]bool
            mu      sync.Mutex
        }
        
        func (jm *JobManager[ID, Result]) StartJob(id ID) bool {
            jm.mu.Lock()
            defer jm.mu.Unlock()
            
            if jm.running[id] {
                return false // 既に実行中
            }
            
            jm.running[id] = true
            return true
        }
    
  12. 動的プログラミング(メモ化)
    • 結果の保存と再利用
        func Fibonacci[T comparable, R comparable](n T, memo map[T]R, calc func(T) R) R {
            if result, found := memo[n]; found {
                return result
            }
            
            result := calc(n)
            memo[n] = result
            return result
        }
    

注意点

[編集]
  • 非比較型の制約: スライス、マップ、関数など、comparableに含まれない型があります
      // これらの型はcomparableでない
      var a []int
      var b []int
      // a == b // コンパイルエラー
      
      var c map[string]int
      var d map[string]int
      // c == d // コンパイルエラー
      
      var e func()
      var f func()
      // e == f // コンパイルエラー(nilとの比較は可能)
    
  • 構造体の比較可能性: 構造体がcomparableであるためには、全てのフィールドがcomparableである必要があります
      type Safe struct {
          ID   int
          Name string
      } // comparable
      
      type Unsafe struct {
          ID   int
          Data []byte
      } // not comparable (スライスフィールドがあるため)
    
  • インターフェースの比較可能性: 動的な型がcomparableでない場合、実行時パニックが発生する可能性があります
      var x, y interface{} = []int{1, 2, 3}, []int{1, 2, 3}
      // x == y  // 実行時パニック - スライスは比較不可
    
  • 実行時vs.コンパイル時の検証: comparableは主にコンパイル時の型チェックを提供します
      // 比較可能性の検証
      func SafeCompare[T comparable](a, b T) bool {
          return a == b  // コンパイル時に安全性が保証される
      }
    
  • ユーザー定義型の比較動作: 比較動作を変更したい場合、型を定義して適切なメソッドを実装する必要があります(演算子オーバーロードは不可)
      type Vector []float64 // スライスベースで比較不可
      
      // 独自の比較メソッド
      func (v Vector) Equals(other Vector) bool {
          if len(v) != len(other) {
              return false
          }
          for i := range v {
              if v[i] != other[i] {
                  return false
              }
          }
          return true
      }
    
  • NaN値の比較: IEEE 754によると、NaNは自分自身と等しくありません
      nan := math.NaN()
      fmt.Println(nan == nan)  // false
      
      // NaNを含む可能性のある比較では注意
      func SafeFloatCompare[T ~float32 | ~float64](a, b T) bool {
          if math.IsNaN(float64(a)) && math.IsNaN(float64(b)) {
              return true // NaNの特別扱い
          }
          return a == b
      }
    
  • ゼロ値との比較: 比較可能な型のゼロ値も比較可能です
      func IsZero[T comparable](v T) bool {
          var zero T
          return v == zero
      }
    
  • 型パラメータと代入可能性: comparable制約は代入可能性を保証するものではありません
      func DoSomething[T comparable](value T) {
          var x int = 10
          // value = x // コンパイルエラー - TがIntとは限らない
      }
    

comparableはGoのジェネリックプログラミングにおいて、等値比較が可能な型を表現するための重要な組み込み制約です。マップのキー型やセットの要素型など、比較可能性が必要なコンテキストで特に有用です。ただし、非比較型の処理や、より細かな比較動作が必要な場合は、追加の型制約やカスタムメソッドを併用する必要があることを理解しておくことが重要です。