操作して学ぶ
同じ処理を10言語で
言語の違いは、機能一覧を眺めても掴めません。いちばん早いのはまったく同じことをさせて、書き方を並べて見比べることです。 FizzBuzz・コレクション処理・ファイルの行数・並行処理という4つの課題を、Python から Haskell まで10言語で用意しました。 課題を切り替えると、ループ・条件分岐・資源管理・並行の単位を それぞれの言語が何で表すのか——その“クセ”が浮かび上がります。
FizzBuzz
1から15までを出力し、3の倍数は Fizz、5の倍数は Buzz、両方の倍数は FizzBuzz に置き換える。ループ・条件分岐・出力という最小の3要素で、各言語の「地の文法」が見える。
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)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);
}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タグ無し 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)
}
}
}(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}"),
}
}
}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;
}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';
}
}すべてはクラスの中。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);
}
}
}トップレベル文(.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);
}ループの代わりに [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)は、 実際に実行して結果を検証済みです。