Go言語 有理数計算 big.Rat.Sub() の実践的プログラミング例

2025-06-01

big.Rat.Sub() は、Go の標準パッケージである math/big に含まれる型 Rat (有理数) のメソッドの一つです。このメソッドは、2つの有理数の差を計算し、その結果をレシーバ (メソッドを呼び出した Rat 型の変数) に格納する 役割を持っています。

より具体的に説明すると、次のような処理を行います。

  1. レシーバ (呼び出し元の Rat)
    a という big.Rat 型の変数が a.Sub(b, c) のように呼び出された場合、a がレシーバとなります。
  2. 引数
    Sub() メソッドは通常、2つの big.Rat 型の引数を取ります。上記の例では bc です。
  3. 計算
    Sub() は、最初の引数 (b) から二番目の引数 (c) を減算した結果、つまり $\(b - c\)$ を計算します。
  4. 結果の格納
    計算された差は、レシーバである a に格納されます。元の a の値は上書きされます。

メソッドのシグネチャ

func (z *Rat) Sub(a, b *Rat) *Rat

  • *Rat: 戻り値も Rat 型へのポインタです。これは、レシーバ (z) 自身へのポインタを返します。メソッドチェーンを可能にするための設計です。
  • (a, b *Rat): これらは引数で、減算される2つの Rat 型へのポインタです。
  • (z *Rat): これはレシーバを示しており、Rat 型へのポインタです。計算結果はこの Rat 型の変数 (z) に格納されます。

使用例

package main

import (
	"fmt"
	"math/big"
)

func main() {
	a := big.NewRat(5, 2)   // a = 5/2
	b := big.NewRat(3, 4)   // b = 3/4
	c := new(big.Rat)       // 結果を格納する Rat 型変数

	// c = a - b を計算
	c.Sub(a, b)
	fmt.Printf("%s - %s = %s\n", a.String(), b.String(), c.String()) // 出力: 5/2 - 3/4 = 7/4

	d := big.NewRat(1, 3)
	e := big.NewRat(1, 6)

	// a = d - e を計算 (レシーバが更新される)
	a.Sub(d, e)
	fmt.Printf("%s - %s = %s\n", d.String(), e.String(), a.String()) // 出力: 1/3 - 1/6 = 1/6
}
  • 引数とレシーバはすべて big.Rat 型のポインタである必要があります。big.NewRat() 関数などで生成された Rat 型の変数のアドレス (&) を渡すか、new(big.Rat) で初期化されたポインタを使用します。
  • Sub() メソッドは、レシーバの値を変更します。もし元の値を保持したい場合は、事前にコピーを作成する必要があります。
  • big.Rat は、標準の float64 などの浮動小数点数型と異なり、正確な有理数を表現できます。したがって、誤差が発生しません。


引数の型が *big.Rat でない

  • トラブルシューティング

    • 引数として渡す変数が big.Rat 型の値である場合は、アドレス演算子 & を使用してポインタを取得してください。
    • big.NewRat() 関数は *big.Rat 型の値を返すため、通常はこちらを使用します。
    a := big.NewRat(5, 2)
    b := big.NewRat(3, 4)
    c := new(big.Rat)
    c.Sub(a, b) // 正しい
    // c.Sub(*a, *b) // 間違い (値型を渡している)
    
  • エラーメッセージの例

    cannot use a (type big.Rat) as type *big.Rat in argument to z.Sub
    
  • エラーの状況
    Sub() メソッドの引数には、big.Rat 型の値ではなく、*big.Rat 型のポインタを渡す必要があります。誤って値型を渡すと、コンパイルエラーが発生します。

レシーバが nil ポインタである

  • トラブルシューティング

    • new(big.Rat) を使用して Rat 型のポインタを初期化してから Sub() メソッドを呼び出すようにしてください。
    • 関数内で Rat 型のポインタを返す場合、nil が返される可能性がないか確認してください。
    var r *big.Rat // nil ポインタ
    // r.Sub(a, b) // パニックが発生する可能性
    
    r = new(big.Rat) // 初期化
    r.Sub(a, b)     // 正しい
    
  • エラーメッセージの例

    panic: runtime error: invalid memory address or nil pointer dereference
    [signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x...]
    
  • エラーの状況
    Sub() メソッドを呼び出すレシーバ (z の部分) が nil ポインタの場合、実行時にパニックが発生します。

意図しないレシーバの変更

  • トラブルシューティング

    • 元の値を保持する必要がある場合は、新しい big.Rat 型の変数を作成し、Set() メソッドなどを使用してレシーバの値をコピーしてから Sub() を呼び出してください。
    a := big.NewRat(5, 2)
    b := big.NewRat(3, 4)
    result := new(big.Rat)
    
    originalA := new(big.Rat).Set(a) // a の値をコピー
    result.Sub(a, b)
    fmt.Printf("Result: %s, Original a: %s\n", result.String(), originalA.String())
    
  • エラーの状況
    Sub() メソッドはレシーバの値を上書きします。元のレシーバの値を保持したい場合、Sub() を呼び出す前にコピーを作成する必要があります。

大きすぎるまたは複雑すぎる有理数の計算

  • トラブルシューティング
    • 扱う有理数の範囲や複雑さを再検討し、本当に big.Rat が必要かどうか検討してください。
    • 計算のアルゴリズムを見直し、より効率的な方法がないか検討してください。
  • エラーの状況
    big.Rat は非常に大きな数や複雑な有理数を扱うことができますが、極端な場合にはメモリを大量に消費したり、計算に時間がかかりすぎたりする可能性があります。

文字列からの変換エラー (関連する問題)

  • トラブルシューティング

    • SetString() メソッドを使用する前に、入力文字列が正しい有理数の形式 (例: "1/2", "-3/4", "5") であることを確認してください。
    • エラーハンドリングを適切に行い、変換に失敗した場合の処理を実装してください。
    r := new(big.Rat)
    _, ok := r.SetString("invalid/format")
    if !ok {
        fmt.Println("Error: Invalid rational number format")
    }
    
  • エラーの状況
    big.Rat を文字列から生成する際に、不正な形式の文字列を渡すとエラーが発生します。これは Sub() 自体のエラーではありませんが、big.Rat を扱う上でよく遭遇する問題です。



例1: 基本的な減算

この例では、2つの big.Rat 型の有理数を作成し、Sub() メソッドを使ってそれらの差を計算し、結果を表示します。

package main

import (
	"fmt"
	"math/big"
)

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

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

	// a から b を減算し、結果を result に格納
	result.Sub(a, b)

	// 結果を表示
	fmt.Printf("%s - %s = %s\n", a.String(), b.String(), result.String()) // 出力: 5/2 - 3/4 = 7/4
}

解説

  • a.String(), b.String(), result.String() は、big.Rat 型の値を人間が読みやすい文字列形式で返します。
  • result.Sub(a, b) は、「result は a から b を引いた値になる」という意味です。
  • new(big.Rat) は、big.Rat 型のゼロ値を持つポインタを返します。このポインタが減算の結果を格納するレシーバとなります。
  • big.NewRat(numerator, denominator) 関数を使って、分子と分母を指定して新しい big.Rat 型の有理数を作成します。

例2: レシーバの再利用

この例では、減算の結果を新しい変数に格納するのではなく、メソッドを呼び出したレシーバ自身に格納します。

package main

import (
	"fmt"
	"math/big"
)

func main() {
	a := big.NewRat(7, 3)
	b := big.NewRat(1, 6)

	// a から b を減算し、結果を a に格納 (a の値が更新される)
	a.Sub(a, b)

	fmt.Printf("7/3 - 1/6 = %s\n", a.String()) // 出力: 7/3 - 1/6 = 13/6

	c := big.NewRat(9, 5)
	d := big.NewRat(2, 5)

	// c から d を減算し、結果を c に格納
	c.Sub(c, d)
	fmt.Printf("9/5 - 2/5 = %s\n", c.String()) // 出力: 9/5 - 2/5 = 7/5
}

解説

  • 変数の値を再利用することで、メモリの使用量を抑えることができます。ただし、元の値を保持しておきたい場合は、事前にコピーを作成する必要があります。
  • a.Sub(a, b) のように、レシーバ自身を最初の引数に指定することで、減算の結果が a に上書きされます。

例3: 複数の減算を連鎖させる (メソッドチェーン)

Sub() メソッドは、レシーバへのポインタ (*big.Rat) を返すため、メソッドチェーンを使って複数の減算を連続して行うことができます。

package main

import (
	"fmt"
	"math/big"
)

func main() {
	a := big.NewRat(1, 1) // 1/1 = 1
	b := big.NewRat(1, 2) // 1/2
	c := big.NewRat(1, 3) // 1/3

	result := new(big.Rat)

	// result = a - b - c を計算
	result.Sub(a, b).Sub(result, c) // 最初の減算結果を次の減算の引数に

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

解説

  • その返されたポインタに対して .Sub(result, c) が呼び出され、現在の result から c が引かれます。
  • result.Sub(a, b)result から b を引いた結果を result に格納し、その result へのポインタを返します。

注意点
メソッドチェーンを使う場合、計算の順序が重要になります。上記の例では、(a - b) - c の順で計算が行われています。

例4: 関数内で big.Rat.Sub() を使用する

big.Rat.Sub() は、他の関数内でも通常通り使用できます。

package main

import (
	"fmt"
	"math/big"
)

// 2つの有理数の差を計算して返す関数
func subtractRats(r1, r2 *big.Rat) *big.Rat {
	result := new(big.Rat)
	return result.Sub(r1, r2)
}

func main() {
	x := big.NewRat(8, 5)
	y := big.NewRat(3, 10)

	difference := subtractRats(x, y)
	fmt.Printf("%s - %s = %s\n", x.String(), y.String(), difference.String()) // 出力: 8/5 - 3/10 = 13/10
}
  • subtractRats 関数は、2つの *big.Rat 型の引数を取り、それらの差を計算した新しい *big.Rat 型の値を返します。


big.Rat.Add() を用いた減算

減算は、第二項の符号を反転させて加算することと同じです。big.Rat 型には符号を反転させるメソッド Neg() が用意されているため、これと Add() メソッドを組み合わせることで Sub() と同様の処理が可能です。

package main

import (
	"fmt"
	"math/big"
)

func main() {
	a := big.NewRat(5, 2)
	b := big.NewRat(3, 4)
	result := new(big.Rat)
	negB := new(big.Rat).Neg(b) // b の符号を反転

	result.Add(a, negB) // a + (-b) = a - b

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

解説

  • result.Add(a, negB) は、a と符号が反転した b (negB) を加算します。これにより、実質的に a - b の減算と同じ結果が得られます。
  • b.Neg(negB) は、b の符号を反転させた結果を negB に格納します。

利点

  • 減算の概念を「符号反転と加算」として捉えることで、より基本的な演算の組み合わせで処理を実現できます。

欠点

  • 処理の意図が Sub() ほど直接的ではありません。
  • Neg()Add() の2つのメソッド呼び出しが必要になるため、わずかにコード量が増えます。

独自の減算関数を実装する (非推奨)

big.Rat は内部的に分子と分母を保持しているため、これらの値を直接操作して減算を行うことも理論的には可能です。ただし、分母の共通化や約分などの処理を自力で実装する必要があり、非常に複雑でエラーが発生しやすいため、通常はこの方法は推奨されません。

package main

import (
	"fmt"
	"math/big"
)

// (a/b) - (c/d) = (ad - bc) / bd を自力で計算 (簡略化のため約分は省略)
func subtractRatsManually(aNum, aDen, bNum, bDen int64) *big.Rat {
	num := new(big.Int).Sub(new(big.Int).Mul(big.NewInt(aNum), big.NewInt(bDen)), new(big.Int).Mul(big.NewInt(bNum), big.NewInt(aDen)))
	den := new(big.Int).Mul(big.NewInt(aDen), big.NewInt(bDen))
	return new(big.Rat).SetFrac(num, den)
}

func main() {
	aNum := int64(5)
	aDen := int64(2)
	bNum := int64(3)
	bDen := int64(4)

	result := subtractRatsManually(aNum, aDen, bNum, bDen)
	fmt.Printf("%d/%d - %d/%d = %s\n", aNum, aDen, bNum, bDen, result.String()) // 出力: 5/2 - 3/4 = 14/8 (約分前)
}

解説

  • 重要な注意点
    この例では結果の約分処理を省略しているため、big.Rat.Sub() の結果とは異なる表現になる可能性があります。また、負の分母の扱いなど、より複雑なケースに対応するにはさらに多くのコードが必要になります。
  • big.Int 型を使って分子と分母の計算を行い、最後に big.NewRat().SetFrac()big.Rat 型の値を生成しています。
  • 上記の例では、2つの有理数 baと dcの減算を、数学的な定義 bdad−bcに基づいて実装しています。

利点

欠点

  • big.Rat パッケージの提供する機能を利用しないため、恩恵を受けられない。
  • 約分などの重要な処理を自力で行う必要があるため、効率が悪い。
  • 実装が複雑で、バグが発生しやすい。

big.Rat.Sub() の直接的な代替手段としては、big.Rat.Add()big.Rat.Neg() を組み合わせる方法が考えられます。しかし、可読性や効率性を考慮すると、big.Rat.Sub() をそのまま使用するのが最も推奨される方法です。