操作して学ぶ

同じ処理を10言語で

言語の違いは、機能一覧を眺めても掴めません。いちばん早いのはまったく同じことをさせて、書き方を並べて見比べることです。 FizzBuzz・コレクション処理・ファイルの行数・並行処理という4つの課題を、Python から Haskell まで10言語で用意しました。 課題を切り替えると、ループ・条件分岐・資源管理・並行の単位を それぞれの言語が何で表すのか——その“クセ”が浮かび上がります。

FizzBuzz

1から15までを出力し、3の倍数は Fizz、5の倍数は Buzz、両方の倍数は FizzBuzz に置き換える。ループ・条件分岐・出力という最小の3要素で、各言語の「地の文法」が見える。

言語で絞り込み
Pythonスクリプト

range(1, 16) は終端を含まない。インデントがそのままブロック構造で、波括弧も行末セミコロンも要らない。

for i in range(1, 16):
    if i % 15 == 0:
        print("FizzBuzz")
    elif i % 3 == 0:
        print("Fizz")
    elif i % 5 == 0:
        print("Buzz")
    else:
        print(i)
JavaScriptスクリプト

C系の三部 for 文と === による厳密等価。console.log はブラウザでも Node でも動く。

for (let i = 1; i <= 15; i++) {
  if (i % 15 === 0) console.log("FizzBuzz");
  else if (i % 3 === 0) console.log("Fizz");
  else if (i % 5 === 0) console.log("Buzz");
  else console.log(i);
}
Rubyスクリプト

Range#each にブロックを渡す。elsif の綴りに注意。if 自体も値を返す式で、すべてがオブジェクト。

(1..15).each do |i|
  if i % 15 == 0
    puts "FizzBuzz"
  elsif i % 3 == 0
    puts "Fizz"
  elsif i % 5 == 0
    puts "Buzz"
  else
    puts i
  end
end
Goシステム

タグ無し switch は if / else if の連鎖を綺麗に畳む慣用句。演算子まわりの空白も gofmt が強制する。

package main

import "fmt"

func main() {
	for i := 1; i <= 15; i++ {
		switch {
		case i%15 == 0:
			fmt.Println("FizzBuzz")
		case i%3 == 0:
			fmt.Println("Fizz")
		case i%5 == 0:
			fmt.Println("Buzz")
		default:
			fmt.Println(i)
		}
	}
}
Rustシステム

(i%3, i%5) のタプルをパターンマッチ。_ は「何でもよい」を表し、分岐の網羅性をコンパイラが検査する。

fn main() {
    for i in 1..=15 {
        match (i % 3, i % 5) {
            (0, 0) => println!("FizzBuzz"),
            (0, _) => println!("Fizz"),
            (_, 0) => println!("Buzz"),
            _ => println!("{i}"),
        }
    }
}
Cシステム

printf は書式文字列で型を明示(%d)。改行も自分で書く。最も低水準で、すべてが明示的。

#include <stdio.h>

int main(void) {
    for (int i = 1; i <= 15; i++) {
        if (i % 15 == 0)      printf("FizzBuzz\n");
        else if (i % 3 == 0)  printf("Fizz\n");
        else if (i % 5 == 0)  printf("Buzz\n");
        else                  printf("%d\n", i);
    }
    return 0;
}
C++システム

std::cout << ... << の連鎖でストリームに流し込む。C由来だが入出力の作法が変わる。

#include <iostream>

int main() {
    for (int i = 1; i <= 15; ++i) {
        if (i % 15 == 0)      std::cout << "FizzBuzz\n";
        else if (i % 3 == 0)  std::cout << "Fizz\n";
        else if (i % 5 == 0)  std::cout << "Buzz\n";
        else                  std::cout << i << '\n';
    }
}
JavaJVM・.NET

すべてはクラスの中。public static void main(String[]) というお決まりの入口が要る。

public class FizzBuzz {
    public static void main(String[] args) {
        for (int i = 1; i <= 15; i++) {
            if (i % 15 == 0)      System.out.println("FizzBuzz");
            else if (i % 3 == 0)  System.out.println("Fizz");
            else if (i % 5 == 0)  System.out.println("Buzz");
            else                  System.out.println(i);
        }
    }
}
C#JVM・.NET

トップレベル文(.NET 6+)で main の定型が消えた。中身は Java とよく似る。

for (int i = 1; i <= 15; i++)
{
    if (i % 15 == 0)      Console.WriteLine("FizzBuzz");
    else if (i % 3 == 0)  Console.WriteLine("Fizz");
    else if (i % 5 == 0)  Console.WriteLine("Buzz");
    else                  Console.WriteLine(i);
}
Haskell関数型

ループの代わりに [1..15] を mapM_ で写す。分岐はガード(| 条件 = 値)で書く純粋関数。

main :: IO ()
main = mapM_ (putStrLn . fizzbuzz) [1 .. 15]
  where
    fizzbuzz n
      | n `mod` 15 == 0 = "FizzBuzz"
      | n `mod` 3 == 0  = "Fizz"
      | n `mod` 5 == 0  = "Buzz"
      | otherwise       = show n

この課題で見えること

同じ if/else でも、Go はタグ無し switch、Rust はタプルのパターンマッチ、Haskell はガードと、条件分岐の“推し”が言語ごとに違う。出力ひとつ取っても print / puts / fmt.Println / std::cout / System.out.println と語彙が割れる。

見比べるときのヒント

  • 同じ課題の中で差を見る。上のボタンで課題を切り替え、「同じことをさせたときの書き方の違い」だけに注目すると、言語の個性が際立ちます。
  • 系統でまとめて眺める。「言語で絞り込み」でスクリプト/システム/JVM・.NET/関数型に絞ると、 思想の近い言語ほど書き方も似ることが見えます。
  • 「何を強制され、何を隠してくれるか」を見る。資源管理(ファイルの課題)と並行(並行の課題)が最も差の出るところ。 C は後始末を手で書かされ、Python や C# は仕組みが裏に隠れます。

掲載しているコードは、各言語の慣用的な書き方に寄せています。出力が一意に決まる課題(コレクション処理=220/並行処理=14)は、 実際に実行して結果を検証済みです。