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

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

コミット

big: usable zero Rat values without need for explicit initialization

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/696ced50feea2e9711636deeb5a5835cfeb998e9

元コミット内容

commit 696ced50feea2e9711636deeb5a5835cfeb998e9
Author: Robert Griesemer <gri@golang.org>
Date:   Fri Oct 21 14:11:36 2011 -0700

    big: usable zero Rat values without need for explicit initialization
    
    - no explicit API change, but new(big.Rat) now creates a big.Rat value
      of 0 that is immediately usable, in sync. w/ the conventions elsewhere
    - various cleanups along the way
    
    R=r
    CC=golang-dev
    https://golang.org/cl/5301056

変更の背景

このコミットは、Go言語のmath/bigパッケージにおける重要な設計改善を実装しています。2011年10月21日、Robert Griesemerによって行われたこの変更は、big.Rat型のゼロ値を明示的な初期化なしに使用可能にすることを目的としています。

問題点

変更前のbig.Ratのゼロ値は、内部的に0/0(ゼロ除算)を表現していました。これは以下の問題を引き起こしていました:

  1. 不正な値: 0/0は数学的に未定義であり、有効な有理数ではありません
  2. 初期化の必要性: 開発者はbig.Ratを使用する前に必ず明示的に初期化する必要がありました
  3. 一貫性の欠如: Go言語の他の型(特にbig.Int)は、ゼロ値がそのまま使用可能であるという設計原則に従っていましたが、big.Ratだけが例外でした
  4. 潜在的なバグ: 初期化を忘れた場合、実行時にゼロ除算エラーが発生する可能性がありました

Go言語の設計哲学との整合性

Go言語には「ゼロ値を有用にする(Make the zero value useful)」という重要な設計原則があります。これは、変数を宣言した時点で、明示的な初期化なしに即座に使用可能な状態にするという考え方です。この原則により:

  • コードがより簡潔になる
  • 初期化忘れによるバグを防げる
  • メモリ効率が良い(ゼロ初期化は単純なmemsetで実現可能)

前提知識の解説

1. big.Rat型について

big.Ratは任意精度の有理数(分数)を表現する型です。構造は以下の通りです:

type Rat struct {
    a Int  // 分子(numerator)
    b nat  // 分母(denominator)、常に正の値
}

2. nat型について

nat型は自然数(非負整数)を表現する基本型で、[]Wordのエイリアスです。重要な特徴:

  • 正規化された表現では、0は空スライス(len(nat) == 0)で表される
  • これにより、メモリ効率の良い表現が可能

3. ゼロ値の概念

Go言語では、変数宣言時に明示的な初期値を与えない場合、その型の「ゼロ値」が自動的に設定されます:

  • 数値型: 0
  • 文字列: ""
  • ポインタ、スライス、マップ、チャネル、関数: nil
  • 構造体: 各フィールドがゼロ値

4. 有理数の正規化

有理数は複数の表現が可能です(例:2/4 = 1/2 = 3/6)。効率的な演算のため、通常は最簡分数形式に正規化します:

  • 分子と分母の最大公約数(GCD)で両方を割る
  • 分母は常に正の値にする
  • 0は0/1として表現

技術的詳細

1. ゼロ値の新しい解釈

主要な変更点は、分母bの長さが0(len(b) == 0)の場合、それを1として扱うという規則の導入です:

type Rat struct {
    a Int
    b nat // len(b) == 0 acts like b == 1
}

これにより、初期化されていないRat0/1 = 0を表すようになりました。

2. 遅延初期化戦略

分母が実際に必要になるまで、空のスライスのままにしておくことで、メモリ効率を向上させています。必要に応じて以下のタイミングで初期化されます:

  • 演算時
  • 文字列表現の生成時
  • 明示的なアクセス時(Denom()メソッド)

3. 関数の改善

mulDenom関数の新設

分母同士の乗算を行う際、ゼロ値(空スライス)を1として扱う専用関数:

func mulDenom(z, x, y nat) nat {
    switch {
    case len(x) == 0:
        return z.set(y)  // x=1なので、結果はy
    case len(y) == 0:
        return z.set(x)  // y=1なので、結果はx
    }
    return z.mul(x, y)   // 通常の乗算
}

scaleDenom関数の新設

整数と分母の乗算を行う関数(旧mulNat関数の改良版):

func scaleDenom(x *Int, f nat) *Int {
    var z Int
    if len(f) == 0 {
        return z.Set(x)  // f=1なので、xのコピーを返す
    }
    z.abs = z.abs.mul(x.abs, f)
    z.neg = x.neg
    return &z
}

4. 正規化処理の改善

norm()メソッドが、より効率的なswitch文ベースの実装に変更されました:

func (z *Rat) norm() *Rat {
    switch {
    case len(z.a.abs) == 0:
        // 分子が0の場合:符号を正に、分母を空スライスに
        z.a.neg = false
        z.b = z.b.make(0)
    case len(z.b) == 0:
        // 既に正規化された整数値
    case z.b.cmp(natOne) == 0:
        // 分母が1の場合:空スライスに正規化
        z.b = z.b.make(0)
    default:
        // 通常の正規化(GCDで約分)
        if f := gcd(z.a.abs, z.b); f.cmp(natOne) != 0 {
            z.a.abs, _ = z.a.abs.div(nil, z.a.abs, f)
            z.b, _ = z.b.div(nil, z.b, f)
        }
    }
    return z
}

コアとなるコードの変更箇所

1. Rat構造体のコメント変更(rat.go:172-177)

// 変更前
// A Rat represents a quotient a/b of arbitrary precision.
// The zero value for a Rat, 0/0, is not a legal Rat.

// 変更後
// A Rat represents a quotient a/b of arbitrary precision.
// The zero value for a Rat represents the value 0.
type Rat struct {
    a Int
    b nat // len(b) == 0 acts like b == 1
}

2. SetInt/SetInt64メソッドの変更(rat.go:208-218)

// 変更前
z.b = z.b.setWord(1)

// 変更後
z.b = z.b.make(0)  // 空スライスで1を表現

3. Denomメソッドの変更(rat.go:266-271)

func (x *Rat) Denom() *Int {
    if len(x.b) == 0 {
        return &Int{abs: nat{1}}  // 空の場合は1を返す
    }
    return &Int{abs: x.b}
}

4. IsIntメソッドの変更(rat.go:243)

// 変更前
return len(x.b) == 1 && x.b[0] == 1

// 変更後
return len(x.b) == 0 || x.b.cmp(natOne) == 0

5. Invメソッドの改善(rat.go:226-234)

a := z.b
if len(a) == 0 {
    a = a.setWord(1)  // 分母を実体化
}
b := z.a.abs
if b.cmp(natOne) == 0 {
    b = b.make(0)     // 分母を正規化
}
z.a.abs, z.b = a, b

6. nat型の初期化方法の統一(全体)

// 変更前
nat(nil).method()

// 変更後
nat{}.method()  // より明確で一貫性のある記法

コアとなるコードの解説

1. ゼロ値表現の工夫

このコミットの核心は、「空スライスを1として解釈する」という巧妙な設計です。これにより:

  • メモリ効率: 分母が1の有理数(実質的に整数)に対して、余分なメモリを割り当てない
  • 後方互換性: APIの変更なしに動作を改善
  • パフォーマンス: 多くの場合で不要な割り当てを回避

2. 遅延評価の活用

分母の実体化を必要な時まで遅らせることで:

var r big.Rat            // r = 0/1(内部的には{0, nil})
r.SetInt64(42)          // r = 42/1(内部的には{42, nil})
r.SetFrac64(3, 4)       // r = 3/4(内部的には{3, [4]})

整数値として使用されている限り、分母のメモリ割り当ては発生しません。

3. 演算の最適化

mulDenomscaleDenom関数により、空スライスの特別扱いが演算全体で一貫して行われます:

// 例:1/2 + 3/1 の計算
// 内部表現: {1, [2]} + {3, nil}
// scaleDenom(&1, nil) = 1(nilは1として扱われる)
// scaleDenom(&3, [2]) = 6
// 結果:7/2

4. エラー処理の統一

ゼロ除算のチェックが各メソッドで明示的に行われるようになりました:

if len(babs) == 0 {
    panic("division by zero")
}

5. テストの充実

新しいテスト関数TestZeroRatが追加され、ゼロ値の動作が包括的に検証されています:

  • ゼロ値同士の比較
  • 文字列表現の確認
  • 演算(加算、減算、乗算)
  • ゼロ除算の例外処理

関連リンク

参考にした情報源リンク

  1. Go's Zero Values - Go言語公式のゼロ値に関する説明
  2. Robert Griesemer Wikipedia - コミット作者の情報
  3. math/big package source - bigパッケージの現在のソースコード
  4. Go FAQ - Design - Go言語の設計に関するFAQ
  5. Understanding Zero Values in Go - ゼロ値の詳細な解説
  6. Code review 5301056 discussion - このコミットに関する開発者間の議論