精度が重要な計算に!Go言語 big.Rat.Mul() の実践例

2025-06-01

Mul() メソッドの主な役割は、2つの big.Rat 型の値を掛け算することです。

具体的には、以下のような働きをします。

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

このメソッドのシグネチャ(関数の型)を見ると、以下の点がわかります。

  • *Rat: メソッドは、掛け算の結果が格納された z へのポインタを返します。通常は、メソッドを呼び出した z がそのまま返されるため、メソッドチェーン(method chaining)が可能です。
  • (x, y *Rat): これは、掛け合わせる2つの Rat 型の値へのポインタです。これらの値はメソッド内で変更されません。
  • (z *Rat): これは、メソッドが呼び出されるレシーバ(receiver)です。掛け算の結果はこの z に格納されます。zRat 型へのポインタであるため、メソッド内で z の値が変更されます。

処理の流れ

z.Mul(x, y) を呼び出すと、内部的には以下の計算が行われます。

もし、x が baで、y が dcであれば、z は以下のようになります。

z=x×y=ba​×dc​=b×da×c​

掛け算の結果である新しい分数 b×da×cは、z が指す Rat 型の値として更新されます。また、結果の分数は、共通の約数で割られるなどして、可能な限り簡約化されます。

使用例

package main

import (
	"fmt"
	"math/big"
)

func main() {
	r1 := big.NewRat(1, 2)   // 1/2 を作成
	r2 := big.NewRat(3, 4)   // 3/4 を作成
	result := new(big.Rat) // 結果を格納する新しい Rat を作成

	result.Mul(r1, r2) // r1 と r2 を掛け算し、結果を result に格納

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

この例では、r1 に 21​、r2 に 43を設定し、Mul() メソッドを使ってこれらを掛け合わせています。結果として、result に 2×41×3​=83が格納され、出力されています。

big.Rat.Mul() を使うことで、浮動小数点数の演算では避けられない可能性のある精度誤差を気にすることなく、正確な有理数の掛け算を行うことができます。金融計算や科学技術計算など、精度が重要な場面で役立ちます。



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

    • エラー
      big.Rat 型のポインタが nil の状態で Mul() を呼び出そうとすると、ランタイムパニックが発生します。
    • 原因
      big.Rat 型の変数を new(big.Rat) で初期化せずに宣言したり、関数から nil のポインタが返されたりする可能性があります。
    • トラブルシューティング
      • big.Rat 型の変数を使用する前に、必ず new(big.Rat) で初期化するか、既存の big.Rat 型の値のアドレスを代入してください。
      • 関数から big.Rat 型のポインタが返る場合は、それが nil でないことを確認してから使用してください。
    var r *big.Rat // 初期化されていない nil ポインタ
    // r.Mul(otherRat, anotherRat) // これはパニックを引き起こす
    
    r = new(big.Rat) // 正しい初期化
    r.Mul(otherRat, anotherRat)
    
  1. オペランドが nil の場合

    • エラー
      Mul() メソッドに渡す引数 (x または y) が nil の場合、nil ポインタの参照となり、ランタイムパニックが発生する可能性があります。
    • 原因
      掛け算に使用する big.Rat 型の値が適切に初期化されていない、または意図せず nil になっている可能性があります。
    • トラブルシューティング
      • Mul() に渡す big.Rat 型のポインタが nil でないことを事前に確認してください。
    var r1 *big.Rat = big.NewRat(1, 2)
    var r2 *big.Rat // 初期化されていない nil ポインタ
    result := new(big.Rat)
    
    // result.Mul(r1, r2) // これはパニックを引き起こす可能性が高い
    
    if r2 != nil {
        result.Mul(r1, r2)
    } else {
        fmt.Println("r2 が nil です")
    }
    
  2. 意図しない値による計算

    • エラー
      big.NewRat()Rat を作成する際に、分子や分母に意図しない値を設定してしまうと、Mul() の結果も期待通りになりません。
    • 原因
      コーディングミスや、外部からの入力値を適切に検証していない場合に起こりえます。
    • トラブルシューティング
      • big.NewRat() に渡す引数(分子と分母)の値が正しいことをログ出力やデバッグツールで確認してください。
      • 外部からの入力に基づいて Rat を作成する場合は、入力値の範囲や形式を検証してください。
    numerator := 1
    denominator := 0 // これはエラーを引き起こしませんが、不正な Rat を作成します
    r := big.NewRat(int64(numerator), int64(denominator))
    // 以降の r を使った計算は予期せぬ結果になる可能性があります
    
    if denominator == 0 {
        fmt.Println("分母が 0 です")
    } else {
        r := big.NewRat(int64(numerator), int64(denominator))
        // ...
    }
    
  3. 結果の簡約化に関する誤解

    • 誤解
      Mul() の結果は常に完全に簡約化されると期待している場合、表示される文字列が期待と異なることがあります。
    • 原因
      big.Rat は内部的に常に簡約化された形式で値を保持しますが、String() メソッドで文字列化された際に、必ずしも人間が最も簡潔だと感じる形式になるとは限りません(例えば、64は内部的には 32となりますが、元の数値から直接文字列化すると "4/6" となる可能性もあります)。
    • トラブルシューティング
      • Rat の内部表現は簡約化されていることを理解してください。
      • 特定の形式で文字列化したい場合は、自分でフォーマット処理を行う必要があるかもしれません。
  4. 大きな数値の扱い

    • 潜在的な問題
      big.Rat は任意の精度を扱えますが、非常に大きな分子や分母を持つ Rat 同士の掛け算は、計算時間やメモリ使用量が増加する可能性があります。
    • 原因
      非常に大きな数を扱う演算は、計算資源を多く消費する可能性があります。
    • トラブルシューティング
      • 扱う数値の範囲を把握し、必要以上に大きな数を扱わないように注意してください。
      • パフォーマンスが重要な場合は、アルゴリズムを見直すことも検討してください。

デバッグのヒント

  • 単体テスト
    big.Rat.Mul() を含む関数に対して、様々な入力パターン(正常なケース、エッジケース、エラーが起こりうるケースなど)に対する単体テストを書くことで、早期に問題を発見できます。
  • デバッガ
    Go のデバッガ(例えば Delve)を使用すると、ステップ実行や変数の検査が可能になり、問題の原因を特定しやすくなります。
  • ログ出力
    計算の途中経過や、big.Rat の値を String() メソッドで文字列化して出力することで、何が起こっているかを確認できます。


基本的な掛け算の例

package main

import (
	"fmt"
	"math/big"
)

func main() {
	// 2つの有理数を作成: 1/2 と 3/4
	r1 := big.NewRat(1, 2)
	r2 := big.NewRat(3, 4)

	// 結果を格納する新しい Rat を作成
	result := new(big.Rat)

	// r1 と r2 を掛け算し、結果を result に格納
	result.Mul(r1, r2)

	// 結果を出力
	fmt.Printf("%s * %s = %s\n", r1.String(), r2.String(), result.String())
	// 出力: 1/2 * 3/4 = 3/8
}

この例では、big.NewRat() 関数を使って 21と 43の2つの有理数を作成しています。その後、result.Mul(r1, r2) を呼び出すことで、r1r2 の積が result に格納されます。最後に、それぞれの有理数と積を文字列として出力しています。

メソッドチェーンの例

package main

import (
	"fmt"
	"math/big"
)

func main() {
	// 3つの有理数を作成
	r1 := big.NewRat(1, 3)
	r2 := big.NewRat(2, 5)
	r3 := big.NewRat(3, 7)

	// 結果を格納する Rat を作成
	result := new(big.Rat)

	// 連続して掛け算を行う (メソッドチェーン)
	result.Mul(r1, r2).Mul(result, r3)

	// 結果を出力
	fmt.Printf("%s * %s * %s = %s\n", r1.String(), r2.String(), r3.String(), result.String())
	// 出力: 1/3 * 2/5 * 3/7 = 6/105
}

Mul() メソッドは、掛け算の結果が格納されたレシーバ (result ポインタ) を返すため、メソッドチェーンを使って複数の掛け算を連続して行うことができます。この例では、31​×52​×73の計算を行っています。

整数との掛け算の例

big.Int 型の整数を big.Rat と掛け合わせる場合、まず big.Int を分母が 1 の big.Rat に変換する必要があります。

package main

import (
	"fmt"
	"math/big"
)

func main() {
	// 有理数 3/5 を作成
	r := big.NewRat(3, 5)

	// 整数 2 を作成
	i := big.NewInt(2)

	// 整数を分母が 1 の Rat に変換
	rInt := new(big.Rat).SetInt(i)

	// 結果を格納する Rat を作成
	result := new(big.Rat)

	// r と rInt を掛け算
	result.Mul(r, rInt)

	// 結果を出力
	fmt.Printf("%s * %s = %s\n", r.String(), rInt.String(), result.String())
	// 出力: 3/5 * 2/1 = 6/5
}

ここでは、big.Int 型の inew(big.Rat).SetInt(i) を使って 12の big.Rat 型に変換しています。その後、通常の Mul() メソッドで掛け算を行います。

ループ内での掛け算の例

package main

import (
	"fmt"
	"math/big"
)

func main() {
	// 初期値
	current := big.NewRat(1, 1)

	// 係数のリスト
	coefficients := []*big.Rat{
		big.NewRat(1, 2),
		big.NewRat(2, 3),
		big.NewRat(3, 4),
		big.NewRat(4, 5),
	}

	// 結果を格納する Rat
	result := new(big.Rat).Set(current) // 初期値をコピー

	// ループで係数を掛け合わせる
	for _, coeff := range coefficients {
		result.Mul(result, coeff)
		fmt.Printf("現在の値: %s\n", result.String())
	}

	fmt.Printf("最終結果: %s\n", result.String())
	// 出力 (途中経過は省略):
	// 最終結果: 1/5
}

この例では、スライス coefficients に含まれる複数の有理数を、ループを使って順番に掛け合わせています。Set() メソッドを使って初期値を result にコピーしている点に注意してください。

package main

import (
	"fmt"
	"math/big"
)

// 2つの Rat を掛け算して結果を返す関数
func multiplyRats(r1, r2 *big.Rat) *big.Rat {
	result := new(big.Rat)
	return result.Mul(r1, r2)
}

func main() {
	r1 := big.NewRat(5, 6)
	r2 := big.NewRat(7, 8)

	product := multiplyRats(r1, r2)

	fmt.Printf("%s * %s = %s\n", r1.String(), r2.String(), product.String())
	// 出力: 5/6 * 7/8 = 35/48
}


直接的な算術演算の実装

big.Rat 型は内部的に分子 (num) と分母 (den) を big.Int 型として保持しています。したがって、Mul() メソッドを使わずに、これらの内部フィールドに直接アクセスして掛け算を実装することも可能です。ただし、この方法では結果の簡約化を自分で行う必要があるため、注意が必要です。

package main

import (
	"fmt"
	"math/big"
)

func multiplyRatsManually(r1, r2 *big.Rat) *big.Rat {
	// 新しい Rat を作成
	result := new(big.Rat)

	// 分子同士、分母同士を掛け算
	num := new(big.Int).Mul(r1.Num(), r2.Num())
	den := new(big.Int).Mul(r1.Den(), r2.Den())

	// 結果を設定
	result.SetFrac(num, den) // SetFrac は結果を簡約化する

	return result
}

func main() {
	r1 := big.NewRat(1, 2)
	r2 := big.NewRat(3, 4)

	product := multiplyRatsManually(r1, r2)

	fmt.Printf("%s * %s = %s\n", r1.String(), r2.String(), product.String())
	// 出力: 1/2 * 3/4 = 3/8
}

この例では、r1.Num()r1.Den()r2.Num()r2.Den() を使ってそれぞれの分子と分母を取得し、big.Int.Mul() で掛け算を行っています。そして、result.SetFrac(num, den) を使って新しい big.Rat に結果を設定しています。SetFrac() は与えられた分子と分母から big.Rat を作成し、自動的に簡約化を行います。

注意点

  • 直接 Num()Den() にアクセスして新しい big.Rat を作成する場合、簡約化を自分で行う必要があります。上記の例のように SetFrac() を使用すれば自動的に簡約化されます。もし自分で簡約化を行う場合は、最大公約数 (GCD) を計算し、分子と分母をそれぞれ GCD で割る必要があります。

他の big.Rat のメソッドの組み合わせ

big.Rat 型には、掛け算以外の算術演算を行うメソッドも用意されています。例えば、加算 (Add)、減算 (Sub)、除算 (Quo) などです。複雑な計算を行う場合、これらのメソッドを組み合わせて目的の処理を実現できますが、単純な掛け算の代替にはなりません。

外部ライブラリの利用 (一般的ではない)

Go の標準ライブラリである math/big は、任意の精度の数値を扱うための強力な機能を提供しており、通常はこれだけで十分です。有理数演算に特化した外部ライブラリも存在する可能性はありますが、big.Rat が十分に高機能であるため、積極的に代替ライブラリを探す必要性は低いでしょう。もし利用する場合は、そのライブラリの API や性能特性を十分に理解する必要があります。