倭算数理研究所

科学・数学・学習関連の記事を、「倭マン日記」とは別に書いていくのだ!

「2つのボールをぶつけると円周率がわかる」のを他の言語でシミュレーションしてみた

ここ1年くらいまとものコードを書いてなかったので、ちょっとリハビリに「「2つのボールをぶつけると円周率がわかる」らしいのでシミュレーションしてみた」で書いていた Groovy コードを他の言語で書き直してみました。

そのまま移植するだけだとあまりにもチャチいコードになったので、引数が複数与えられていればそれらに対してシミュレーションを行い、引数がなければ標準入力から値を受け取るように少々改善。 なんだかほとんど標準入力の処理の覚え書きみたいな感じになってますが・・・

Java

まずは Java から。 2つの質点の速度を長さ2の配列として保持していますが、衝突による速度の変化はその配列の成分を直接書き換えています。 つまりはガッツリ副作用のあるコードです。 引数の処理に少し Stream API を使ってるので要 Java8 以上。 Java10 で導入された var はなんとなく使ってません。

import java.util.Arrays;
import java.util.Scanner;

public class CollisionPi {

    private final int n;
    private final double r;  // 100^N

    public CollisionPi(int n){
        this.n = n;
        this.r = Math.pow(100, n);
    }

    public void simulate(){
        double[] v = new double[]{1.0, 0.0};
        int count = 0;
        boolean  flag = true;  // collide1, collide2 を交互に呼び出すのに使うフラグ

        while(v[0] > v[1] || v[1] > 0.0){
            if(flag) collide1(v);
            else     collide2(v);
            
            flag = !flag;
            count++;
        }

        System.out.printf("N = %d のとき、衝突回数は %d%n", this.n, count);
    }

    // 質点同士の衝突
    private void collide1(double[] v){
        double w0 = ((r-1.0)*v[0] + 2.0*v[1])/(r+1.0);
        double w1 = (2.0*r*v[0] - (r-1.0)*v[1])/(r+1.0);
        v[0] = w0;
        v[1] = w1;
    }

    // 質点2と壁との衝突
    private void collide2(double[] v){
        v[1] = - v[1];
    }

    // main 関数
    public static void main(String... args){
        if(args.length > 0){
            Arrays.stream(args).forEach(CollisionPi::simulate);

        }else{
            Scanner sc = new Scanner(System.in);
            while(true){
                System.out.print("Nの値を入力してください(終了は \"exit\"):");
                String s = sc.next();
                if(s.equals("exit"))break;
                simulate(s);
            }
        }
    }

    private static void simulate(String s){
        try{
            int n = Integer.parseInt(s);
            new CollisionPi(n).simulate();

        }catch(NumberFormatException e){
            System.out.printf("数値に変換できません:%s%n", s);
        }
    }
}

引数として「0 1 2 3 4 5 6 7」を与えて実行すると、以下のように表示されます:

N = 0 のとき、衝突回数は 3
N = 1 のとき、衝突回数は 31
N = 2 のとき、衝突回数は 314
N = 3 のとき、衝突回数は 3141
N = 4 のとき、衝突回数は 31415
N = 5 のとき、衝突回数は 314159
N = 6 のとき、衝突回数は 3141592
N = 7 のとき、衝突回数は 31415926

うまく円周率の数値がでてきましたね。 よしよし。 引数を与えずに実行した場合の出力は省略。 

Scala

Scala でも 上記の Java コードのように書けますが、ここでは再帰を使って関数型プログラミングっぽく書いてみましょう。 2つの質点の速度は List として持ち、衝突前後で別の List オブジェクトにします。

import java.util.Scanner

class CollisionPi(n: Int){
  private val r = Math.pow(100.0, n)  // r = 100^N

  // 質点同士の衝突
  private def collision1(v: List[Double]): List[Double] =
    List(
      ((r - 1.0) * v(0) + 2.0 * v(1)) / (r + 1.0),
      (2.0 * r * v(0) - (r - 1.0) * v(1)) / (r + 1.0)
    )

  // 質点2と壁との衝突
  private def collision2(v: List[Double]): List[Double] = List(v(0), -v(1))

  def simulate(): Unit = {

    def simulate(v: List[Double], flag: Boolean, count: Int): Int =
      if(v(0) > v(1) || v(1) > 0.0) {
        val w = if(flag) collision1(v) else collision2(v)
        simulate(w, !flag, count + 1)
      }else{
        count
      }

    val result = simulate(List(1.0, 0.0), true, 0)
    printf("N=%d のとき、衝突回数は %d%n", n, result)
  }
}


object CollisionPi extends App{

  def simulate(s: String): Unit = {
    parseIntOption(s) match {
      case Some(n) => new CollisionPi(n).simulate()
      case None => printf("整数値に変換できません:%s%n", s)
    }
  }

  if(args.length > 0){
    // 引数が与えられていれば、それぞれの値に対してシミュレーションを実行
    args.foreach(simulate)
  }else{
    // 引数が与えられていなければ、標準入力から値を受け取る
    val sc = new Scanner(System.in)

    def loop(): Unit = {
      print("""N の値を入力してください(終了は "exit"): """)
      sc.next() match {
        case "exit" =>
        case s =>
          simulate(s)
          loop()
      }
    }

    loop()
  }

  // ユーティリティ
  def parseIntOption(s: String): Option[Int] = {
    try{
      Some(s.toInt)
    }catch{
      case e: NumberFormatException => None
    }
  }
}

Scala での標準入力の処理は Java の場合と同じなので特に問題はないでしょう。 また、必ずしも必要がないのですが、与えられた N の値が数値に変換できるかどうかを Option で返すユーティリティ関数 parseIntOption を定義して入力を検証しています。

まぁ、これくらいのコードなら再帰を使って書いても簡単にも複雑にもならない感じですかねw

実行結果は Java の場合と同じなので省略(Go, Rust の場合も同じ)。

Go

Go でのコードは、ポインタがある以外は Java とそんなに変わらない感じで書けます(というか、これくらいのコードだとそういう風にしか書けないというか・・・)。 2つの質点の速度は(要素数が変わらないので)スライスではなく配列で持っていれば充分でしょう。 ここでも Java の場合と同じように、衝突によって速度が変わっても同じ配列の要素を書き換えています(新たに配列を作ったりしていません)。

package main

import (
	"fmt"
	"math"
	"os"
	"strconv"
)

type CollisionPi struct {
	n int
	r float64
}

func NewSimulation(n int) CollisionPi {
	return CollisionPi{n, math.Pow10(2 * n)}
}

// 質点同士の衝突
func (self *CollisionPi) collide1(v *[2]float64) {
	r := self.r
	w0 := ((r-1.0)*v[0] + 2.0*v[1]) / (r + 1.0)
	w1 := (2.0*r*v[0] - (r-1.0)*v[1]) / (r + 1.0)
	v[0] = w0
	v[1] = w1
}

// 質点2と壁との衝突
func (self *CollisionPi) collide2(v *[2]float64) {
	v[1] = -v[1]
}

func (self *CollisionPi) Simulate() {
	v := [2]float64{1.0, 0.0}
	count := 0
	flag := true

	for v[0] > v[1] || v[1] > 0.0 {
		if flag {
			self.collide1(&v)
		} else {
			self.collide2(&v)
		}

		flag = !flag
		count++
	}

	fmt.Printf("N=%d のとき、衝突回数は %d\n", self.n, count)
}

func main() {
	if len(os.Args) > 1 {
                // 引数が与えられていれば、それぞれの値に対してシミュレーションを実行
		for _, s := range os.Args[1:] {
			simulate(s)
		}
	} else {
                // 引数が与えられていなければ、標準入力から値を受け取る
		for {
			fmt.Print("Nの値を入力してください(終了は\"exit\"):")
			var s string
			fmt.Scan(&s)
			if s == "exit" {
				break
			}
			simulate(s)
		}
	}
}

func simulate(s string) {
	n, err := strconv.Atoi(s)
	if err != nil {
		fmt.Printf("数値に変換できません:%s\n", s)
		return
	}

	NewSimulation(n).Simulate()
}

標準入力から値を読み出すのは fmt.Scan を使うんですね。 その他、文字列を数値に変換したり、べき乗を計算したりする関数がなんだったか全く覚えてなかったな・・・

Rust

最近お勉強中の Rust。 関数型プログラミング的な言語機能がたくさんあるので Scala っぽく再帰で書こうかと思ったのですが、なんだかオーバーフローを起こしたのでここではおとなしく while 文でループしておきます。 2つの質点の速度はやはり配列で持ち、衝突前後で配列の要素を変更しています。

struct CollisionPi {
    n: usize,
    r: f64
}

impl CollisionPi {

    fn new(n: usize) -> CollisionPi {
        return CollisionPi{ n, r: 100_f64.powf(n as f64) };
    }

    // 質点同士の衝突
    fn collide1(&self, v: &mut[f64; 2]){
        let r = self.r;
        let w0 = ((r - 1.0) * v[0] + 2.0 * v[1]) / (r + 1.0);
        let w1 = (2.0 *r * v[0] - (r -1.0) * v[1]) / (r + 1.0);
        v[0] = w0;
        v[1] = w1;
    }

    // 質点2と壁との衝突
    fn collide2(&self, v: &mut[f64; 2]){
        v[1] = -v[1];
    }

    pub fn simulate(&self) {
        let mut v: [f64; 2] = [1.0, 0.0];
        let mut count: usize = 0;
        let mut flag: bool = true;

        while v[0] > v[1] || v[1] > 0.0 {
            if flag {
                self.collide1(&mut v)
            } else {
                self.collide2(&mut v)
            }

            count += 1;
            flag = !flag;
        }

        println!("N={} のとき、衝突回数は {}", self.n, count);
    }
}

use std::io;

fn main() {
    let args = std::env::args();
    if args.len() > 1 {
        // 引数が与えられていれば、それぞれの値に対してシミュレーションを実行
        args.skip(1).for_each(simulate);

    } else {
        // 引数が与えられていなければ、標準入力から値を受け取る
        loop {
            println!("Nの値を入力してください:");

            let mut buffer = String::new();
            io::stdin().read_line(&mut buffer).unwrap();

            let s = buffer.trim().to_string();
            if s == "exit" {break;}

            simulate(s);
        }
    }
}

fn simulate(s: String){
    match s.parse() {
        Ok(n) => CollisionPi::new(n).simulate(),
        Err(_) => println!("数値に変換できません:{}", s)
    }
}

標準入力からの読み込みは std::io::Stdin の read_line で行うようですね。 Result で例外処理できるのはやっぱり便利ですね。 あと、可変参照のために「mut」を書くのが結構面倒なので、可能ならやっぱり関数型スタイルで書いた方がいい気もしますね。

今回は Java, Scala, Go, Rust で同じコードを書いてみました。 同じような宣言や文でも各言語によって微妙に書き方が違うことから、各言語でそれぞれどんな機能やコンセプトを大事にしているのかが分かりますね・・・というのが理想的な感想な気がしますが、まぁ実際には「型宣言にコロン必要だっけ?」とか「String? string?」みたいにまごつくことが多々あるのが現実ですかね。

プログラミングRust

プログラミングRust