プログラミング言語とは
目次
- 概要
- プログラミング言語とは何か
- 言語を見る5つの軸
- 型とは何か
- 静的型付けと動的型付け
- 型推論と多相性
- 評価戦略
- スコープと束縛
- メモリ管理
- 所有権と借用
- 例外とエラー処理
- 関数型プログラミング
- オブジェクト指向
- 並行性とメモリモデル
- 代表的な言語をどう比較するか
- 小さなコード比較
- 比較ケース
- 追加トピック
- よくある誤解
- 具体的な言語設計の比較
- まとめ
- 参考文献
概要
まず、この章の中心構造を図で確認します。細部に入る前に、どの概念がどこへつながるかをつかむための地図です。
型・評価戦略・所有権・GC・関数型を軸に整理する
プログラミング言語を学ぶとき、文法や人気言語の違いだけを追っても全体像はつかみにくいです。ここでは、型、評価戦略、メモリ管理、並行性、抽象化という軸で言語を見ていきます。
この章の役割は、「言語がどんな設計思想で作られているか」をつかむことです。処理系が内部でどう実装するかは必要最小限にとどめ、詳しくは次の コンパイラ で扱います。
言語の違いは、見た目の書き方より「何を安全にしたいか」「いつ計算するか」「メモリをどう扱うか」に現れます。型システム、所有権、GC、関数型の考え方が見えると、新しい言語も比較しやすくなります。
この章で重視すること
- 言語を「文法の違い」ではなく「設計思想の違い」として見る
- 静的型付けと動的型付けを対立でなくトレードオフとして理解する
- 所有権、借用、GC、参照カウントの役割差を整理する
- 関数型とオブジェクト指向を宗教論にせず、抽象化の道具として捉える
- 言語の内部実装に入りすぎず、設計思想とトレードオフの理解に集中する
プログラミング言語とは何か
プログラミング言語は、計算を人間が扱いやすい形で表現し、その意味を処理系が実行できるようにするための道具です。
ここで重要なのは、言語は単なる記法ではないという点です。言語は次のものを含みます。
- 構文
- 意味
- 型の規則
- 実行時の約束
- 標準ライブラリ
- エラー時の振る舞い
たとえば x = x + 1 という見た目が似ていても、言語によって
- 変数が再代入可能か
- オーバーフローがどう扱われるか
- 並行実行中に安全か
- 実行時に型検査されるか
が大きく違います。
言語を見る5つの軸
言語を比較するときは、少なくとも次の5軸を見ると整理しやすくなります。
-
型 静的か動的か、強いか弱いか、多相性をどう扱うか。
-
評価戦略 式をいつ評価するか。呼び出し時か、必要になるまで遅らせるか。
-
抽象化 関数、オブジェクト、モジュール、型クラス、トレイトなどをどう組み合わせるか。
-
並行性 スレッド、アクターモデル、async/await、共有メモリ、メッセージパッシングをどう設計するか。
型とは何か
型は、「この値に対してどんな操作が意味を持つか」を表す約束です。
たとえば整数なら加算や減算が自然ですが、ファイルハンドルやネットワークソケットは別の操作体系を持ちます。型は、
- 値の分類
- 許される操作
- エラーの早期検出
- 最適化のための情報
を与えます。
型があると何がうれしいか
- 明らかな誤りを早く検出できる
- IDEや補完が賢くなる
- 最適化しやすくなる
- APIの意図が表面化する
型は安全性だけのためではない
型はしばしば「安全のため」と説明されますが、それだけではありません。型は設計の境界を表し、コードレビューやリファクタリング時の判断材料にもなります。
型をコンパイラが実際にどう扱うか は、コンパイラ の 意味解析と型検査 をあわせて読むと立体的になります。
型を見るときの主要な観点
型システムは単純に「ある / ない」で語れるものではありません。少なくとも次の観点で見ると整理しやすくなります。
| 観点 | 何を見るか | 代表例 |
|---|---|---|
| 検査時期 | 実行前か実行時か | Rust / Python |
| 多相性 | 汎用性をどう与えるか | Generics / trait / interface |
| 部分型 | A を B として扱えるか |
Java / TypeScript |
| null性 | nullを型でどう扱うか | Kotlin / Rust / Java |
| 効果 | 例外・IO・可変性をどう見せるか | Haskell / Rust |
型の直感をつかむための見方
型は「値のラベル」ではなく、「この値に対して許される未来の操作の集合」だと考えると分かりやすくなります。
たとえば整数の型は、
- 足し算できる
- 比較できる
- 配列添字に使えるかもしれない
という未来を持っています。一方でファイルハンドル型は、
- 読み込める
- 閉じられる
- 複製できるかもしれない
という別の未来を持っています。
型エラーは「禁止」より「契約違反」
型エラーを「コンパイラがうるさい」と感じることがありますが、本質的には契約違反の早期発見です。たとえば、
len(42)
が失敗するのは、len が想定している契約に 42 が入っていないからです。型システムは、この契約違反を早めに見つけるための装置です。
静的型付けと動的型付け
静的型付け
プログラム実行前、またはコンパイル時に型検査を行います。
代表例:
長所:
- 早い段階でエラーが分かる
- 大規模変更に強い
- 最適化しやすい
短所:
- 型定義や注釈が重くなることがある
- 柔軟な試行錯誤にはやや向かない場合がある
動的型付け
実行時に値の型を確かめながら動きます。
代表例:
- Python
- Ruby
- JavaScript
長所:
- 小さな試作やREPLで扱いやすい
- 記述量が少なく済むことがある
短所:
- 実行パスに入るまでエラーが見えないことがある
- 大規模化したときの保証が弱くなりやすい
よくある誤解
静的型付けが常に優れていて、動的型付けが劣るわけではありません。探索的なデータ分析、スクリプト、DSL的な用途では動的型付けの軽さが効きます。
強い型付けと弱い型付け
静的 / 動的とは別に、「暗黙変換をどれくらい許すか」という軸があります。
- 強い型付け: 危険な暗黙変換をあまり許さない
- 弱い型付け: 文脈に応じた変換を比較的多く許す
ただし、この用語は人によって使い方が揺れやすいので、実務では「どの変換を許すのか」を具体的に言った方が誤解が少ないです。
静的型付けの中でもかなり違う
同じ静的型付けでも、言語ごとに性格は大きく違います。
つまり「静的型付きだから似ている」とは限りません。どこまで型に責任を持たせるかが違います。
型推論と多相性
型推論は、明示的に全部書かなくても処理系が型を導く仕組みです。
let x = 1
の x を整数と推論するのが典型例です。
多相性
同じ関数や型が、複数の型に対して働ける性質です。
- アドホック多相: オーバーロード、トレイト、型クラス
- パラメトリック多相: ジェネリクス
- 部分型多相: 継承やinterface
多相性はコード再利用のためだけでなく、抽象化の境界を自然に保つためにも使われます。
型推論は何をしているのか
型推論は魔法ではなく、式どうしの制約を集めて整合性を解く作業です。
たとえば
let add1 x = x + 1
なら、
1は整数+は両辺が加算可能であることを要求- したがって
xも整数
という制約が伝播します。
型推論が強いと何が起きるか
- 型注釈を減らせる
- ただしエラーメッセージが抽象的になることがある
- 高度な抽象化を多用すると、逆に明示注釈の方が読みやすい場合もある
ジェネリクスとモノモーフィゼーション
多相性を実装するときの代表的な方法に、モノモーフィゼーション があります。これは
Vec<int>Vec<string>
のように、使われた型ごとに具体的な実装を生成する方式です。
利点:
- 実行時オーバーヘッドが小さい
- 最適化しやすい
欠点:
- 生成コードが大きくなりやすい
RustやC++ のテンプレートはこの色が強いです。一方、Javaのgenericsは型消去寄りの設計です。
この違いは、実行時コスト、生成コード量、抽象化のしやすさに影響します。
部分型多相とパラメトリック多相の違い
- 部分型多相: 「犬は動物である」のような関係を使う
- パラメトリック多相:
「どんな型
Tに対しても同じ構造で動く」を使う
この違いはAPI設計に大きく効きます。前者は振る舞いの差を許しやすく、後者はより一様な抽象化を作りやすいです。
評価戦略
評価戦略は、「式をいつ計算するか」です。
eager evaluation
関数呼び出し時に引数を先に評価します。多くの実用言語はこれです。
lazy evaluation
値が本当に必要になるまで計算を遅らせます。Haskellが代表例です。
call by valueとcall by name
- call by value: 値を先に計算して渡す
- call by name: 式そのものを遅らせて渡す
この違いは性能だけでなく、副作用の見え方も変えます。
なぜ評価戦略が重要なのか
初心者のうちは、式は書いた順にそのまま評価されると思いがちです。しかし実際には、
- 先に全部計算するか
- 必要になるまで遅らせるか
- 一度計算した結果を共有するか
で、性能も意味も変わります。
遅延評価で何が起きるか
遅延評価は無限リストや宣言的記述と相性が良い一方、次のような難しさも持ちます。
- どこで実際に計算されるか見えにくい
- メモリ使用量が直感とずれる
- サンクの蓄積で遅くなることがある
call by need
遅延評価にも種類があります。call by need は、一度必要になって計算した結果を再利用する方式です。Haskellはこの考え方に近いです。
副作用と評価戦略
評価戦略は副作用の見え方を変えます。たとえば同じ式でも、
- eagerなら必ず呼ばれる
- lazyなら参照されなければ呼ばれない
という違いが出ます。これはログ出力、例外、外部IOを含む処理で特に重要です。
具体例
if expensive() then 1 else 2
このような式で、条件に不要な部分まで先に計算してしまうかどうかは、言語設計と特別形式の扱いに依存します。
スコープと束縛
変数名がどの値を指すかを決めるルールです。
lexical scope
ソースコード上の位置で束縛が決まる。現代言語の標準です。
dynamic scope
呼び出し履歴で束縛が決まる。理解しにくく、限定用途で使われます。
スコープを理解すると、クロージャがなぜ動くのかが見えてきます。
クロージャ
クロージャは、関数と、その関数が参照していた環境を一緒に閉じ込めたものです。
たとえば
makeAdder(10)
が返す関数は、10 という値を覚えたまま後で実行されます。これは単にコードだけを返しているのではなく、環境も一緒に持ち運んでいるということです。
シャドーイング
内側のスコープで同じ名前を再定義すると、外側を隠すことがあります。これは便利ですが、過度に使うと可読性が落ちます。
束縛と可変性
「名前の再束縛」と「値の可変性」は別問題です。
let x = 1のxを再定義できるかxが指す中身を変更できるか
は別々に設計されることがあります。
メモリ管理
手動管理
Cの malloc/free のように、プログラマが解放責任を持ちます。
長所:
- 予測可能
- 低レベル制御がしやすい
短所:
- 解放漏れ
- 二重解放
- ダングリングポインタ
ガベージコレクション
不要になったオブジェクトを自動回収します。
代表的な方式:
参照カウント
参照数が0になったら解放します。直感的ですが、循環参照に弱いです。
各方式の比較
| 方式 | 長所 | 短所 | 向きやすい領域 |
|---|---|---|---|
| 手動解放 | 予測可能で低レベル制御しやすい | バグを埋め込みやすい | OS、組み込み、低レイテンシ |
| GC | 開発効率が高い | 停止時間やメモリ使用量の制御が難しいことがある | サーバ、業務系、VM言語 |
| 参照カウント | 直感的で局所性がある | 循環参照、更新コスト | UI、共有オブジェクト系 |
| 所有権 | 高い安全性と予測可能性 | 学習コストが高い | システム、インフラ、性能重視 |
GCは全部同じではない
GCという言葉でひとまとめにしがちですが、実際には大きく違います。
たとえばサーバではスループット重視か停止時間重視かで選択が変わります。
メモリ管理は性能にもAPIにも影響する
寿命管理の設計は、内部実装だけでなくAPIの形も変えます。
- コピーを返しやすいか
- 参照を返しやすいか
- 共有更新を許すか
- 非同期処理と相性がよいか
といった判断に直結します。
所有権と借用
Rustで有名になった設計ですが、考え方自体はより広く使えます。
- ある値には基本的に一つの所有者がいる
- 一時的な参照は借用として扱う
- 共有参照と可変参照のルールを厳密にする
これにより、実行時GCなしでメモリ安全をかなり高い水準で確保できます。
所有権が向く領域
- システムプログラミング
- 非常に性能が重要な処理
- リソースリークを強く避けたい環境
借用の直感
借用は、「所有権を渡さずに一時的に見せる」ことです。
- 読み取り専用で何人にも見せる
- 書き換えるなら一人だけに見せる
というルールにすると、データ競合やuse after freeをかなり防げます。
ライフタイム
ライフタイムは、「この参照はどこまで生きてよいか」という期間です。これはメモリ管理を時間軸で明示したものだと考えると分かりやすいです。
所有権はメモリだけの話ではない
所有権の考え方は、ファイルディスクリプタ、ソケット、ロック、GPUバッファなど、メモリ以外のリソース管理にも応用できます。
moveとcopy
所有権ベースの言語では、代入が必ずしもコピーとは限りません。
- 値を複製する
- 所有権を移す
の違いを区別する必要があります。ここを曖昧にすると、「書いたつもりのコード」と「実際に起きていること」がずれます。
例外とエラー処理
言語は失敗をどう表すかでも性格が出ます。
- 例外
- 戻り値による明示
Result/Eitherpanic/abort
例外は便利ですが、制御フローが見えにくくなることがあります。逆に戻り値中心だと明示的ですが、記述量が増えやすいです。
recover可能な失敗と不可能な失敗
失敗には大きく分けて2種類あります。
- 予想される業務的失敗 例: ファイルがない、入力が不正
- プログラムの破綻に近い失敗 例: 不変条件違反、メモリ破壊
前者は Result で返す方が自然なことが多く、後者はpanicや例外で早く落とす方がよい場合があります。
checked exceptionの議論
Javaのchecked exceptionは、失敗を型に出す設計として重要な試みでしたが、実務では過剰な伝播や形骸化も起こしました。ここから分かるのは、「型で失敗を表せば自動的にうまくいく」わけではなく、設計の粒度が重要だということです。
関数型プログラミング
関数型プログラミングは、「全部immutableで書く宗教」ではありません。中心にあるのは次の考え方です。
- 関数を値として扱う
- 副作用を局所化する
- immutableな値を好む
- 合成しやすい形で処理を書く
関数型の利点
- テストしやすい
- 並列化しやすい
- 局所的にreasoningしやすい
関数型の落とし穴
- 抽象化が過剰になると読みづらい
- 遅延評価や高階関数が初心者には追いにくい
不変性がなぜ効くのか
immutableな値を基本にすると、
- いつ誰が書き換えたかを追わなくてよい
- 並行処理で共有しやすい
- テストやキャッシュと相性がよい
という利点があります。
関数は値である
関数型では、関数を引数に渡したり、戻り値にしたり、データ構造に入れたりします。これにより
- map
- filter
- fold
のような高階関数が自然になります。
純粋性
純粋関数は、同じ入力なら必ず同じ出力を返し、副作用を外に漏らさない関数です。純粋性が高いほどreasoningしやすくなります。
モナドは何のためにあるか
モナドはしばしば難解に説明されますが、直感的には「文脈を保ったまま計算をつなげる枠組み」です。
- 失敗するかもしれない
- IOを含む
- 非決定性がある
といった文脈を保ったまま合成するために使います。
オブジェクト指向
オブジェクト指向は、状態と振る舞いを一緒に扱い、責務のまとまりを表現する道具です。
ただし、継承を多用するスタイルだけがOOPではありません。現代では
- interface
- composition
- protocol
- trait
がより重視されます。
OOPの中心はカプセル化
オブジェクト指向の中心は、継承そのものではなく、状態と操作を境界の内側に閉じ込めることです。
継承が難しい理由
継承は強い道具ですが、
- 基底クラスの変更が広く波及する
- 置換可能性が崩れやすい
- 振る舞いの理解が飛びやすい
ため、現代ではcompositionが好まれやすいです。
OOPと関数型は対立しない
現代の多くの言語は、OOPと関数型の要素を混ぜています。
などは、その典型です。現実の設計では、データの安定性には関数型的手法、振る舞いの境界にはOOP的手法、のように使い分ける方が自然です。
並行性とメモリモデル
メモリモデルは、複数スレッドからメモリがどう見えるかの約束です。
ここが曖昧だと、
- 書き込み順序の見え方
- データ競合
- 再順序化
- 原子性
が崩れます。
なぜ重要か
CPUは高速化のために命令やメモリアクセスを並べ替えることがあります。コンパイラも同じです。そのため、「ソースコード順に見えるはず」という直感は並行実行では危険です。
代表的な設計
データ競合とは何か
複数スレッドが同じメモリに同時に触れ、そのうち少なくとも一方が書き込みで、適切な同期がない状態をデータ競合と呼びます。
データ競合があると、
- たまたま動く
- ある日だけ壊れる
- 最適化レベルで挙動が変わる
といった厄介な問題が起きます。
happens before
多くのメモリモデルでは、「この操作はあの操作より前に起きたと見なせる」という happens before 関係が重要です。ロックやチャネル、atomic操作は、この順序を確立するために使われます。
メモリモデルの設計差
- Java: VMと言語仕様の約束でかなり明文化されている
- C/C++: 高性能だが誤用時に危険
- Rust: C/C++ に近い低レベル性能を持ちつつ、型システムで共有可能性を絞る
- Erlang: 共有メモリを避けることで問題設定そのものを変える
asyncとスレッドは別問題
async/await はしばしば並行性と同一視されますが、本質的には待機点を明示するための構文です。どこまで並列に動くか、共有メモリをどう扱うかは別途設計が必要です。
代表的な言語をどう比較するか
まずは比較軸を明示してから個別言語を見ると迷いにくくなります。
| 言語 | 型 | メモリ管理 | 並行性 | 抽象化の色 |
|---|---|---|---|---|
| C | 静的 | 手動 | スレッド中心 | 低レベル手続き型 |
| Rust | 静的 | 所有権 | スレッド + async | traitとzero cost |
| Go | 静的 | GC | goroutine | シンプルな実用主義 |
| Java | 静的 | GC | スレッド + JMM | OOPと巨大基盤 |
| Haskell | 静的 | GC | STMなど | 純粋関数型 |
| Python | 動的 | GCなど | GIL前提の工夫 | 生産性重視 |
C
- 低レベル制御が強い
- 手動メモリ管理
- 抽象化は少なめ
強みは、機械に近い制御と予測可能性です。一方で安全性を多くプログラマ側に委ねます。
Rust
- 所有権と借用で安全性を高める
- ゼロコスト抽象化を重視
「高性能」と「高安全性」を両立したい領域で強いですが、学習初期はborrow checkerが難所になります。
Go
- シンプルさと実用性
- GC
- goroutineとchannel
大規模抽象化より、読みやすさと運用しやすさを優先する設計です。
Java
企業システムや大規模基盤で強く、言語仕様だけでなくJVM全体の力が大きいです。
Haskell
- 純粋関数型
- 強い型システム
- 遅延評価
抽象化の力が非常に強く、理論的な美しさがありますが、実務導入には学習コストがあります。
Python
- 動的型付け
- 可読性と試行錯誤に強い
- 実行性能は処理系や拡張に依存
探索やglue codeで強い一方、大規模化では型ヒントや設計規律が重要になります。
どの言語が良いかではなく、何に向くか
言語選択は優劣より適合性です。
- レイテンシ重視のシステム: Rust / C / C++
- 企業業務システム: Java / C# / Kotlin
- 迅速な自動化や分析: Python
- 高並行サーバ: Go / Erlang / Elixir
- 理論志向や抽象化重視: Haskell / OCaml
小さなコード比較
例1: sum関数
def add(a, b):
return a + b
fn add(a: i64, b: i64) -> i64 {
a + b
}
add a b = a + b
見た目は似ていますが、
- 型注釈の強制
- 評価戦略
- オーバーロード解決
は言語ごとに違います。
例2: 失敗を戻り値で表すか例外で表すか
data = json.loads(text)
let data = serde_json::from_str(text)?;
前者は例外中心、後者は Result を明示的に流す設計です。どちらが良いかは用途次第ですが、制御フローの見え方はかなり違います。
例3: 可変更新とimmutable
xs.append(1)
xs2 = 1 : xs
前者は既存構造を更新し、後者は新しい値を作る感覚です。これがデバッグ、共有、安全性、並行性に響きます。
比較ケース
ケース1: 低レイテンシのネットワークサーバ
GC停止やメモリレイアウトの予測可能性が重要になるケースです。
このとき見たい軸:
RustやC/C++ が候補に上がりやすいのは、この軸で強いからです。一方、開発速度や安全性の取り方は大きく違います。
ケース2: すばやく試作したいデータ処理
要件がまだ揺れていて、まずは小さく試したいときは、
- REPLやスクリプト性
- ライブラリエコシステム
- 記述量の少なさ
が効きます。PythonやJavaScriptが強いのはこの場面です。
ケース3: 長く保守する企業業務システム
チーム人数が多く、変更が長く続くなら、
- 型による保証
- ツール支援
- 長期保守しやすいライブラリ
- 例外や非同期の設計指針
が重要です。Java、Kotlin、C# などが選ばれやすいのはこのためです。
ケース4: 抽象化を強く使いたい数理・DSL領域
型推論、多相性、純粋性、代数的データ型が強いと、抽象化の質が大きく変わります。HaskellやOCaml、Scalaがここで注目されやすいです。
追加トピック
代数的データ型ADT
関数型や型の強い言語で重要なのが、代数的データ型 です。
- 和型:
AまたはB - 積型:
AとB
を明示できるので、状態の取りうる形を型で表しやすくなります。
パターンマッチ
ADTと相性がよいのがパターンマッチです。
match expr with
| Int n -> ...
| Add (x, y) -> ...
のように書けると、分岐条件がデータ構造に密着し、抜け漏れ検査もやりやすくなります。
モジュールと名前空間
大きな言語では、型や関数の設計だけでなく、モジュール境界も重要です。
- 公開 / 非公開
- import / export
- パッケージ管理
- 循環依存の扱い
は、言語の保守性に強く効きます。
効果systemの直感
型が「値の形」だけでなく、「この関数はIOをする」「例外を投げる」「状態を書き換える」といった効果まで表す方向があります。これはeffect systemと呼ばれます。
よくある誤解
- 型が強いほど生産性が低い、とは限らない
- GCがあると遅い、とは限らない
- 関数型は実務向きでない、とは限らない
- OOPは継承中心、ではない
- 低レベル言語ほど必ず速い、とは限らない
速度、保守性、安全性、学習コスト、エコシステムはいつもトレードオフです。
具体的な言語設計の比較
型システムの進化
静的型言語
- Java: クラスベース、nominal typing、generics (型消去)
- C++: テンプレート、コンパイル時多相、型推論
- Rust: 所有権ベース、借用、ライフタイム、trait bounds
- Haskell: 依存型手前、型クラス、型推論
動的型言語
- Python: duck typing、型ヒント(PEP 484+)
- JavaScript: プロトタイプベース、最近は TypeScript
- Lua: メタテーブルで柔軟
静的型の利点:
- コンパイル時エラーキャッチ
- パフォーマンス最適化
- IDE補完の精度
動的型の利点:
- 開発速度
- 柔軟なデータ構造
- スクリプティングに向く
メモリ管理の比較
| 言語 | 方式 | 利点 | 欠点 |
|---|---|---|---|
| C/C++ | 手動 | 高速、細かい制御 | バグの温床 |
| Java/Python | GC | 安全、開発速度 | GC pause、オーバーヘッド |
| Rust | 所有権 | メモリ安全、パフォーマンス | 学習曲線が急 |
| Go | GC + escape analysis | バランス | GC pause発生 |
Rust の所有権システム:
fn main() {
let s1 = String::from("hello");
let s2 = s1; // ownership move
// println!("{}", s1); // compile error
let s3 = String::from("world");
takes_ownership(s3);
// s3 はもう使えない
}
fn takes_ownership(s: String) {
println!("{}", s);
} // sはここで drop されて メモリ解放
並行性モデルの比較
| 言語 | モデル | 例 |
|---|---|---|
| Java | shared memory + monitors | synchronized, volatile |
| Go | CSP (Communicating Sequential Processes) | goroutines + channels |
| Rust | ownership + fearless concurrency | async/await, Arc<Mutex |
| Erlang | actor model | lightweight processes |
| Scala | multi-paradigm | actors (Akka), futures |
Go のシンプルさ(goroutine例):
func main() {
results := make(chan string, 10)
go func() {
results <- "result1"
}()
go func() {
results <- "result2"
}()
for i := 0; i < 2; i++ {
fmt.Println(<-results)
}
}
Erlang の耐障害性:
-module(counter).
-export([start/0, increment/0, get/0]).
start() ->
register(counter, spawn(fun loop/0)).
loop() ->
receive
{increment, From} ->
From ! ok,
loop();
{get, From} ->
From ! Count,
loop()
after 1000 ->
loop()
end.
関数型の要素
Haskell (純粋関数型)
-- すべてが immutable
factorial :: Integer -> Integer
factorial 0 = 1
factorial n = n * factorial (n - 1)
-- list comprehension
squares = [x * x | x <- [1..10]]
-- type class による多態
class Eq a where
(==) :: a -> a -> Bool
instance Eq Bool where
True == True = True
False == False = True
_ == _ = False
Lisp (macro, homoiconicity)
;; コードがデータ
(defmacro when (condition &body body)
`(if ,condition (progn ,@body)))
(when (> x 10)
(print "big")
(setf x (- x 1)))
Scala (関数型 + OOP)
// for-comprehension (関数型的)
val result = for {
x <- List(1, 2, 3)
y <- List(4, 5)
if x * y > 5
} yield x * y
// or equivalent:
List(1, 2, 3).flatMap(x =>
List(4, 5).withFilter(y => x * y > 5)
.map(y => x * y)
)
メタプログラミング能力
Template metaprogramming (C++)
template <int N>
struct Factorial {
static const int value = N * Factorial<N-1>::value;
};
template <>
struct Factorial<0> {
static const int value = 1;
};
// コンパイル時計算
const int fact5 = Factorial<5>::value; // 120
Python の decorator
def memoize(func):
cache = {}
def wrapper(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
return wrapper
@memoize
def fibonacci(n):
if n <= 1:
return n
return fibonacci(n-1) + fibonacci(n-2)
Lisp macro
(defmacro dolist ((var list-form) &body body)
`(let ((list ,list-form))
(while list
(let ((,var (car list)))
,@body)
(setf list (cdr list)))))
パフォーマンス特性の実践的考察
言語選定時のトレードオフ:
| 優先順位 | 推奨言語 | 理由 |
|---|---|---|
| 最高速(ミリ秒単位) | C/C++/Rust | ネイティブコンパイル、細かい制御 |
| バランス | Go/Java | GC あるが予測可能、十分な速度 |
| 開発速度 | Python/TypeScript | 文法簡潔、豊富なライブラリ |
| 安全性重視 | Rust/TypeScript | 型システム、static analysis |
| スクリプティング | Python/Bash | 一行実行、強力な stdlib |
実装の複雑さ
| 言語 | インタプリタ実装難度 | JIT必要度 | 標準ライブラリ規模 |
|---|---|---|---|
| Python | 低(C実装が公式) | 不要だが有用 | 大(stdlib充実) |
| JavaScript | 中(V8など高度) | 必須(性能のため) | 中(Node.js で補完) |
| Go | 中 | 不要 | 中(stdlib充実) |
| Rust | 高(LLVM使用) | 不要 | 中 |
| Ruby | 中 | 有用 | 中 |
過去の失敗から学ぶ
PHP
- もともと template language → 言語へ進化
- グローバル変数が多い、命名規則がバラバラ
- 後付けのOOP(最初はなかった)
- 結果:セキュリティバグが多発
JavaScript on the server
- 元々ブラウザのみ想定
- コールバック地獄(callback hell)
- 型の暗黙変換トラブル(0 == false など)
- 最近:TypeScript + async/await で改善
Java の仗む複雑性
- 単純な処理にもボイラープレートが多い
- 初期の Checked Exception(後悔と明言)
- Null Reference(Tony Hoare が「十億ドルの誤り」と呼んだ)
- 最近:record type、sealed class など改善中
まとめ
プログラミング言語は、文法の違いだけでなく、型、評価戦略、メモリ管理、抽象化の設計思想として見ると整理しやすくなります。新しい言語に出会っても、同じ軸で比較できることがこの章の大きな価値です。
参考文献
公式・標準
論文
書籍
- Benjamin C. Pierce, Types and Programming Languages
- Michael L. Scott, Programming Language Pragmatics