Goプログラミング:big.Rat.Add() で誤差のない計算を実現する

2025-06-01

基本的な使い方

big.Rat.Add() は、以下のような形式で使用します。

func (z *Rat) Add(x, y *Rat) *Rat

このメソッドは、次のような処理を行います。

  1. レシーバ z (呼び出し元の Rat 型の変数) に、引数として与えられた2つの Rat 型の変数 xy の和を代入します。
  2. メソッドの戻り値は、和を格納したレシーバ z へのポインタ *Rat です。つまり、演算結果は呼び出し元の変数自身に格納されます。

具体例

簡単な例を見てみましょう。

package main

import (
	"fmt"
	"math/big"
)

func main() {
	// 2つの有理数を作成 (1/2 と 1/3)
	a := big.NewRat(1, 2)
	b := big.NewRat(1, 3)
	c := new(big.Rat) // 結果を格納する Rat 型の変数を新規作成

	// a と b を足し算し、結果を c に格納
	c.Add(a, b)

	// 結果を出力
	fmt.Printf("%s + %s = %s\n", a.String(), b.String(), c.String()) // 出力: 1/2 + 1/3 = 5/6
}

この例では、以下の処理が行われています。

  1. big.NewRat(1, 2)big.NewRat(1, 3) で、それぞれ有理数 21と 31を表す Rat 型の変数 ab を作成しています。
  2. new(big.Rat) で、演算結果を格納するための新しい Rat 型の変数 c を作成しています。
  3. c.Add(a, b) を呼び出すことで、ab の和 (21​+31​=63+2​=65​) が計算され、その結果が c に格納されます。
  4. c.String() メソッドを使って、Rat 型の値を文字列として出力しています。
  • big.Rat 型の変数は、NewRat() 関数を使って分子と分母を指定して作成するか、new(big.Rat) でゼロ値で初期化してから値を設定する必要があります。
  • Add() メソッドは、レシーバ (z) の値を変更します。もし元の値を保持しておきたい場合は、あらかじめコピーを作成してから Add() を呼び出す必要があります。
  • big.Rat 型は、標準の float64 型などと異なり、任意の精度で有理数を扱うことができます。そのため、浮動小数点数の演算で発生する可能性のある誤差を避けることができます。


一般的なエラーとトラブルシューティング

    • エラー
      Rat 型の変数を new(big.Rat) で初期化せずに、var r *big.Rat のように宣言しただけの状態で r.Add(a, b) を呼び出すと、nil ポインタに対するメソッド呼び出しとなり、ランタイムパニックが発生します。
    • トラブルシューティング
      big.Rat 型の変数を使用する前に、必ず new(big.Rat) で初期化するか、既存の big.Rat 変数のポインタをレシーバとして使用してください。
    // 間違いの例
    var r *big.Rat
    a := big.NewRat(1, 2)
    b := big.NewRat(1, 3)
    // r.Add(a, b) // ランタイムパニックが発生
    
    // 正しい例
    r := new(big.Rat)
    r.Add(a, b)
    
  1. オペランドが nil の場合

    • エラー
      Add() メソッドに渡す引数 (x または y) が nil ポインタである場合、メソッド内で nil ポインタを参照しようとして、ランタイムパニックが発生する可能性があります。
    • トラブルシューティング
      Add() に渡す Rat 型の変数が nil でないことを事前に確認してください。通常は big.NewRat() で作成された Rat 型のポインタが渡されるため、このエラーは比較的稀ですが、変数を直接操作している場合に注意が必要です。
    var a *big.Rat // nil のまま
    b := big.NewRat(1, 3)
    c := new(big.Rat)
    // c.Add(a, b) // ランタイムパニックの可能性
    
    if a != nil && b != nil {
        c.Add(a, b)
    }
    
  2. 期待しない結果 (整数演算との混同)

    • 誤解
      big.Rat は有理数を扱う型であり、整数演算とは異なります。例えば、割り算の結果が整数にならない場合でも、big.Rat は分数として正確に保持します。
    • トラブルシューティング
      big.Rat の演算結果を整数として扱いたい場合は、Int() メソッドなどで整数部分を取得する必要があります。ただし、これは情報の一部を失う可能性があることに注意してください。
    a := big.NewRat(5, 2) // 2.5
    b := new(big.Int)
    b.Set(a.Num())       // 分子を取得 (5)
    // b は整数 5 になります。有理数としての情報は失われます。
    
  3. String() メソッドの出力形式

    • 誤解
      Rat 型の値を fmt.Println() などで直接出力すると、内部表現ではなく、人間が読みやすい分数形式 (例: "1/2", "-3/4") で表示されます。
    • トラブルシューティング
      数値として扱いたい場合は、Float64() メソッドなどで float64 型に変換する必要があります。ただし、精度が失われる可能性があることに注意してください。内部表現を確認したい場合は、Num() (分子) と Denom() (分母) メソッドで big.Int 型の値を取得できます。
    r := big.NewRat(3, 5)
    fmt.Println(r)       // 出力: 3/5
    f, _ := r.Float64()
    fmt.Println(f)       // 出力: 0.6
    num := r.Num()
    denom := r.Denom()
    fmt.Printf("分子: %s, 分母: %s\n", num.String(), denom.String()) // 出力: 分子: 3, 分母: 5
    
  4. 大きな数を扱う際のパフォーマンス

    • 注意点
      big.Rat は任意の精度で数を扱える反面、非常に大きな分子や分母を持つ有理数の演算は、標準の数値型よりも計算コストが高くなる可能性があります。
    • トラブルシューティング
      パフォーマンスが重要な場面では、本当に高精度な有理数演算が必要かどうかを検討し、必要に応じて標準の数値型との使い分けを検討してください。
  5. 符号の扱い

    • 注意点
      big.Rat は符号を正しく扱います。負の数を足し算する場合も、期待通りの結果が得られます。
    • トラブルシューティング
      符号が期待通りでない場合は、入力となる Rat 型変数の符号を改めて確認してください。Sign() メソッドで符号 (1: 正、-1: 負、0: ゼロ) を確認できます。

トラブルシューティングのヒント

  • fmt.Printf() で値を出力
    演算前後の Rat 型の値や、関連する変数の値を String() メソッドで出力して確認することで、どこで期待と異なる動作をしているかを見つけやすくなります。
  • 簡単な例で動作確認
    問題が複雑な場合に、最小限のコードで big.Rat.Add() の動作を確認してみることで、問題の切り分けができます。
  • エラーメッセージをよく読む
    コンパイラやランタイムが出力するエラーメッセージは、問題の原因を特定するための重要な情報源です。


基本的な足し算の例

これは先ほどもご紹介しましたが、改めて基本的な使い方を確認します。

package main

import (
	"fmt"
	"math/big"
)

func main() {
	// 2つの有理数を作成
	a := big.NewRat(1, 2) // 1/2
	b := big.NewRat(1, 3) // 1/3
	result := new(big.Rat)

	// a と b を足し算し、結果を result に格納
	result.Add(a, b)

	fmt.Printf("%s + %s = %s\n", a.String(), b.String(), result.String()) // 出力: 1/2 + 1/3 = 5/6
}

この例では、21と 31を足し算し、その結果である 65を出力しています。

異なる符号の有理数の足し算の例

符号が異なる有理数の足し算も同様に行えます。

package main

import (
	"fmt"
	"math/big"
)

func main() {
	a := big.NewRat(3, 4)   // 3/4
	b := big.NewRat(-1, 2)  // -1/2
	result := new(big.Rat)

	result.Add(a, b)

	fmt.Printf("%s + %s = %s\n", a.String(), b.String(), result.String()) // 出力: 3/4 + -1/2 = 1/4
}

この例では、43と −21を足し算し、結果の 41を出力しています。

整数との足し算の例

big.Int 型の整数を big.Rat に変換してから足し算を行うことができます。

package main

import (
	"fmt"
	"math/big"
)

func main() {
	a := big.NewRat(5, 3) // 5/3
	b := big.NewInt(2)    // 整数 2
	bRat := new(big.Rat).SetInt(b) // big.Int を big.Rat に変換
	result := new(big.Rat)

	result.Add(a, bRat)

	fmt.Printf("%s + %s = %s\n", a.String(), bRat.String(), result.String()) // 出力: 5/3 + 2/1 = 11/3
}

ここでは、整数 2SetInt() メソッドを使って big.Rat 型の 2/1 に変換してから、35と足し算しています。

複数の有理数の足し算の例

複数の有理数を順番に足し算することも可能です。

package main

import (
	"fmt"
	"math/big"
)

func main() {
	a := big.NewRat(1, 5)
	b := big.NewRat(2, 5)
	c := big.NewRat(3, 5)
	result := new(big.Rat)

	result.Add(a, b).Add(result, c) // (a + b) + c

	fmt.Printf("%s + %s + %s = %s\n", a.String(), b.String(), c.String(), result.String()) // 出力: 1/5 + 2/5 + 3/5 = 6/5
}

メソッドチェーンを利用して、Add() の結果をそのまま次の Add() のレシーバとして使用しています。

Add() の結果を既存の変数に上書きする例

Add() メソッドは、レシーバに結果を格納するため、既存の変数を上書きする形で使用することもできます。

package main

import (
	"fmt"
	"math/big"
)

func main() {
	sum := big.NewRat(0, 1) // 初期値 0
	a := big.NewRat(1, 4)
	b := big.NewRat(3, 8)

	sum.Add(sum, a) // sum = sum + a
	fmt.Printf("sum after adding a: %s\n", sum.String()) // 出力: sum after adding a: 1/4

	sum.Add(sum, b) // sum = sum + b
	fmt.Printf("sum after adding b: %s\n", sum.String()) // 出力: sum after adding b: 5/8
}

この例では、sum という変数を初期化し、そこに順次 ab を足し込んでいます。

誤差のない計算の例 (浮動小数点数との比較)

big.Rat は浮動小数点数のような演算誤差がないことを示す簡単な例です。

package main

import (
	"fmt"
	"math/big"
)

func main() {
	r1 := big.NewRat(1, 3)
	r2 := big.NewRat(1, 3)
	r3 := big.NewRat(1, 3)
	sumRat := new(big.Rat)
	sumRat.Add(r1, r2).Add(sumRat, r3)

	floatSum := 1.0/3.0 + 1.0/3.0 + 1.0/3.0

	fmt.Printf("big.Rat sum: %s\n", sumRat.String())   // 出力: big.Rat sum: 1/1
	fmt.Printf("float64 sum: %f\n", floatSum)         // 出力: float64 sum: 1.000000
}

浮動小数点数ではわずかな誤差が生じる可能性がありますが、big.Rat は正確に 31​+31​+31​=1 を計算します。



big.Rat 同士の直接演算 (メソッドチェーン)

package main

import (
	"fmt"
	"math/big"
)

func main() {
	a := big.NewRat(1, 2)
	b := big.NewRat(1, 3)
	c := big.NewRat(1, 6)
	result := new(big.Rat)

	// 複数の Add をチェーンで呼び出す
	result.Add(a, b).Add(result, c)

	fmt.Printf("%s + %s + %s = %s\n", a.String(), b.String(), c.String(), result.String()) // 出力: 1/2 + 1/3 + 1/6 = 1/1
}

この例では、result.Add(a, b) の結果(result が更新される)に対して、さらに .Add(result, c) を呼び出すことで、a + b + c の計算を行っています。

複数の big.Rat を集約して足し算する関数を作成する

package main

import (
	"fmt"
	"math/big"
)

func sumRats(rats []*big.Rat) *big.Rat {
	sum := new(big.Rat)
	for _, r := range rats {
		sum.Add(sum, r)
	}
	return sum
}

func main() {
	rats := []*big.Rat{
		big.NewRat(1, 4),
		big.NewRat(2, 4),
		big.NewRat(3, 4),
	}

	total := sumRats(rats)
	fmt.Printf("合計: %s\n", total.String()) // 出力: 合計: 3/2
}

この例では、sumRats 関数が big.Rat のスライスを受け取り、ループ内で Add() を使ってそれらの合計を計算しています。

他の演算と組み合わせて足し算を行う

直接的な代替ではありませんが、掛け算や割り算の結果に対してさらに足し算を行うなど、より複雑な計算の中で Add() を使用することが一般的です。

package main

import (
	"fmt"
	"math/big"
)

func main() {
	a := big.NewRat(1, 2)
	b := big.NewRat(2, 3)
	c := new(big.Rat)
	d := new(big.Rat)
	result := new(big.Rat)

	c.Mul(a, b)       // c = a * b
	d.SetInt64(1)    // d = 1
	result.Add(c, d)  // result = c + d

	fmt.Printf("(%s * %s) + 1 = %s\n", a.String(), b.String(), result.String()) // 出力: (1/2 * 2/3) + 1 = 4/3
}

この例では、掛け算の結果 (c) に整数 1 (d) を足しています。

外部ライブラリの利用 (稀なケース)

標準パッケージの math/big は十分に高機能ですが、もし特定の高度なニーズがある場合には、有理数演算をサポートするサードパーティのライブラリを探すという選択肢も理論的には存在します。しかし、big.Rat は十分に信頼性が高く、多くのケースで代替ライブラリの必要性は低いでしょう。

  • big.Rat の基本的な足し算は、やはり Add() メソッドを使用するのが最も直接的で効率的な方法です。
  • これらの代替方法は、直接的に「Add() の代わりにこれを使う」というものではなく、Add() メソッドを核として、より複雑な処理や複数の値を扱うためのアプローチです。