
【2026年7月版】
Nimの構文・ディープインサイト ― Pascal・Python・Haskellとどこが似ているのか
公開日: 2026/07/24
読了時間: 約 57分
本稿の範囲
本稿の主題は、Nimの各構文がPascal・Python・C系・関数型言語のどのスタイルにどう似ているかの全域比較です(第5部、全38トピック)。ブロック構文だけでなく、宣言・型システム・式指向・ジェネリクス・メタプログラミング・コンパイル時実行までを対象とします。「どの言語から歴史的に採用したか」という由来には立ち入りません。コーディングする側にとって重要なのは、どのようなスタイルの構文か、だからです。
その前提として、比較対象となる言語の設計を先に整理します。
- PascalとPythonの構文比較(第1部)
- 「Pascalのインデントは整形、Pythonのインデントは構文」という違い(第2部)
- インデントがブロックになる言語(オフサイドルール採用言語)の概観(第3部)
- Haskellのレイアウトルールが糖衣構文であること(第4部)
- Nim構文のスタイル比較(第5部)
Nimのパートでは「見た目が似ている」と「意味論まで似ている」を区別しながら、具体的なコード例をもとに構文ごとに整理します。Nimに関する記述は公式マニュアル・公式チュートリアル上のNim 2.2.10時点の仕様に基づきます。
第1部 PascalとPythonの構文比較
PascalとPythonはどちらも「教育向け」として広く使われた言語ですが、設計思想は異なります。Pascalはアルゴリズムを厳密に記述することを重視し、Pythonは読みやすさと生産性を重視しています。
代表的な構文の対応は次のとおりです。
| 内容 | Pascal | Python |
|---|---|---|
| コメント | {...} または (*...*) | # |
| 変数宣言 | var x: Integer; | x = 0 |
| 定数 | const PI = 3.14; | PI = 3.14(慣習) |
| 代入 | x := 10; | x = 10 |
| 比較 | = | == |
| ブロック | begin ... end | インデント |
| 文末 | ; | 不要 |
条件分岐
if x > 10 then
begin
WriteLn('big');
end
else
begin
WriteLn('small');
end;if x > 10:
print("big")
else:
print("small")繰り返し(for)
for i := 1 to 10 do
begin
WriteLn(i);
end;for i in range(1, 11):
print(i)繰り返し(while)
while x < 10 do
begin
x := x + 1;
end;while x < 10:
x += 1関数
function Add(a, b: Integer): Integer;
begin
Add := a + b;
end;def add(a, b):
return a + bPascalでは関数名に代入することで戻り値を返します(DelphiではResult := ...も利用可能)。この方式は後述するNimのresult変数と対比されます。
配列
var
a: array[1..5] of Integer;a[1] := 10;a = [0] * 5
a[0] = 10Pascalは1始まりの配列も定義できますが、Pythonは常に0始まりです。
レコード(構造体)
type
Person = record
Name: String;
Age: Integer;
end;from dataclasses import dataclass
@dataclass
class Person:
name: str
age: intクラス
type
TPerson = class
public
procedure Hello;
end;class Person:
def hello(self):
print("Hello")構文上の大きな違い
| Pascal | Python |
|---|---|
begin/endでブロックを表現 | インデントでブロックを表現 |
:=で代入 | =で代入 |
=は比較 | ==は比較 |
| 静的型付け | 動的型付け(型ヒントは任意) |
| 変数宣言が必要 | 宣言不要 |
| コンパイル言語 | インタプリタ実行が中心 |
設計思想
Pascalは「プログラムの構造を明示する」ことを重視しており、変数や型を先に宣言してからアルゴリズムを記述します。一方Pythonは「コードを読む人」に重点を置き、宣言を極力省略し、インデントだけで構造が分かるよう設計されています。
Pythonのインデント構文は見た目こそ異なりますが、「ブロックを明確にする」という考え方はPascalのbegin ... endと共通しています。またNimは、Pythonのインデント構文とPascalの静的型付け・コンパイル言語という特徴を組み合わせた設計になっており、両者の中間に位置する言語です。この点は第5部で検証します。
第2部 インデントは「見た目」か「構文」か
第1部の比較表では「ブロック: begin...end vs インデント」と1行で記載しましたが、これは単なる記法の違いではありません。
Pascalのインデントはコンパイラに無視される
標準的なPascalでは、beginの後は通常改行し、endも独立した行に書きます。
begin
x := 1;
y := 2;
end;しかし、インデントは構文上は不要です。コンパイラは空白やインデントを見ません。次のコードも構文的には正しいものです。
begin
x := 1;
y := 2;
end;beginの後に改行しないことも可能です。
begin x := 1; y := 2; end;これも有効なPascalです。ただし、ほぼすべてのPascal/Delphi/FPCのコーディング規約では改行スタイルが推奨されています。beginとendの対応関係が見やすくなり、ネストしたブロックも追いやすくなるためです。
条件分岐でも同様です。
if x > 0 then
begin
print(x);
end;インデントがなくてもコンパイルできます。Pascalにおいてインデントは人間のためのものです。
Pythonのインデントは構文そのもの
一方Pythonでは、インデントが構文そのものです。
if x > 0:
print(x)インデントがないとエラーになります。
この差を「見た目の慣習の違い」として説明するのは不正確です。実際には構文木(AST)の表現方法そのものが違います。
Pascalでは、ブロックは**begin ... endというトークン**で表現されます。
begin
if A then
begin
...
end;
end;インデントは単なる見た目であり、すべて左端に寄せても構文は変わりません。
一方、PythonではブロックはINDENT/DEDENTという字下げそのものです。
if a:
if b:
...ここでは、
- 1段深い = 1つブロックに入る
- 1段戻る = ブロックを抜ける
という関係が成り立っています。つまり、
- Pascalはbegin/endのペアがブロックを作る
- Pythonはインデントの深さそのものがブロックを作る(インデントの深さ = ネストの深さ)
という違いです。
Pythonの字下げは単なるレイアウトではなく、字句解析(Lexer)の段階でINDENTとDEDENTというトークンに変換されます。Pythonにとってインデントは括弧やbegin/endと同じ「構文記号」です。
その意味では、Pythonの構文はPascalよりも、Lispの括弧やC言語の{}に近い役割をインデントが担っていると言えます。これは「見た目の違い」ではなく、「ブロックを何で表現するか」という言語設計の根本的な違いです。
第3部 インデントがブロックになる言語たち — オフサイドルール
Python以外にも、インデントそのものがブロックになる言語は存在します。ただし、Pythonほど普及しているものは多くありません。
Haskell
main = do
print "Hello"
foo x =
if x > 0
then x
else -xHaskellは**レイアウトルール(Layout Rule)**を採用しています。インデントがブロックになりますが、必要なら{}と;で明示することもできます(第4部で詳述)。
Nim
if x > 0:
echo "positive"
else:
echo "negative"NimもPythonと同様にインデントでブロックを表現します。ただし、構文はPascalやAdaにもよく似ています。
F#
if x > 0 then
printfn "positive"
else
printfn "negative"F#もオフサイドルール(Off-side Rule)を採用しています。
Elm
if x > 0 then
"positive"
else
"negative"関数型言語で、インデントが構造になります。
Idris / Agda
証明支援・依存型言語でもインデントベースの構文が採用されています。
CoffeeScript
square = (x) ->
x * xJavaScriptへコンパイルされる言語で、Python風のインデント構文を採用しています。
Cobra
Python風構文を持つ.NET言語です。
共通する考え方
これらの言語は「Off-side Rule(オフサイドルール)」または「Layout Rule(レイアウトルール)」を採用しています。仕組みは次のとおりです。
- 行頭位置をLexerが追跡する
- インデントが増えたらブロック開始
- インデントが減ったらブロック終了
PythonのINDENT/DEDENTもこの考え方に基づいています。
Pascalとの関係で特筆すべきNim
この中で特筆すべきはNimです。Nimは、
- 静的型付け
var、type、procなどPascal系のキーワード- コンパイル言語
というPascalによく似た特徴を持ちながら、ブロック表現だけはbegin/endではなくPythonのようなインデント構文です。そのため、**「Pascalの構造とPythonのレイアウトを融合した言語」**と言われることもあります。
第4部 Haskellのレイアウトルール — インデントは糖衣構文
Haskellでは、{}と;を明示したらインデントは無視されます。
Haskellの**レイアウトルール(インデント)と明示的な{}・;**は、同じ構文を表す2つの記法です。次の2つは等価です。
レイアウトルール版:
let
x = 1
y = 2
in x + y明示版:
let {
x = 1;
y = 2
} in x + y{}を書いた時点で、そのブロック内ではインデントは構文上の意味を持ちません。
let {
x = 1;
y = 2
} in x + yのように字下げがバラバラでも構文としては問題ありません(可読性は別問題です)。
ブロックごとに独立して決まる
この切り替えはブロックごとに決まります。外側はレイアウトルール、内側だけ明示、という混在が可能です。
main = do
putStrLn "A"
let {
x = 1;
y = 2
}
print (x + y)ここでは、
doブロックはインデントで管理letブロックは{}と;で管理
というように、それぞれ独立しています。
Pythonとの違い
Pythonはインデントしかブロック表現を持ちません。一方Haskellは、構文的には{}と;が本来の表現であり、**レイアウトルールはそれを自動的に補う糖衣構文(syntactic sugar)**です。
Haskellのコンパイラは字句解析の段階でレイアウトルールを解釈し、内部的には{、}、;を補ってから構文解析を行います。この設計により、必要な場面では明示記法に切り替えられます。
同じ「インデントベース」でも、Pythonは「インデントが唯一の構文」、Haskellは「インデントは明示記法の省略形」であり、設計の立て付けが異なります。
第5部 Nim構文の分析 — どの言語のどのようなスタイルに似ているのか
Nimの構文を、公式マニュアルと設計資料を軸に、Python・Pascal・C系・関数型言語とどう似ているか・どう違うかという観点で構文ごとに整理します。
結論
Nimの構文は単純に「Pythonの構文+Pascalの型システム」ではありません。より正確には、次の組み合わせです。
- 字面とブロック構造: Python系
- 宣言構文、手続き型の語彙、型の記述順序: Pascal/Modula系
- 静的型付け、値型、ポインタ、FFI、低レベル制御: Pascal・C・C++系
- 式指向、ジェネリクス、パターン的な型制約: 関数型言語・ML系
- UFCS、テンプレート、ASTマクロ、コンパイル時実行: Nim独自色が強い
- 名前解決と記法の柔軟性: 既存言語のどれか一つには還元できない
したがってNimは、Python風のレイアウトを使ってPascal的な宣言型・静的型付きプログラムを書き、Cに近い制御能力とLispに近い構文拡張性を持たせた言語と捉えるのが適切です。
以下、単なる見た目ではなく、構文と意味論を分けて比較します。
1. ブロック構文はPython型だが、実装方式は同一ではない
if temperature > 30:
echo "hot"
startFan()
else:
echo "normal"if temperature > 30:
print("hot")
start_fan()
else:
print("normal")if temperature > 30 then
begin
WriteLn('hot');
StartFan;
end
else
begin
WriteLn('normal');
end;NimではPythonと同じように、コロンの後にインデントを深くすることでブロックを開始します。インデントが元の深さへ戻るとブロックが終了します。
ただし内部実装はPythonと完全に同じではありません。NimではLexerが各トークンに直前の空白数を注釈し、Parserがインデント深度のスタックを管理します。公式マニュアルは、インデント自体を独立したトークンにしないと説明しています。PythonのINDENT/DEDENTトークン方式と目的は同じですが、処理方法は異なります。
Nimでもインデントが単なる整形ではない点は重要です。
if enabled:
initialize()
start()
shutdown()これは次の構造です。
if
└─ initialize
└─ start
shutdown一方、次のコードではshutdown()も条件内です。
if enabled:
initialize()
start()
shutdown()Pythonとの違いとして、まず先頭インデントにタブを使用できません。空白のみです。公式文法も明示的にタブを禁止しています。
if enabled:
<TAB>start() # コンパイルエラー: タブは使えないまた、Haskellのようにブロックを{}と;へ切り替える一般的な代替構文はありません。
# このようなHaskell式の代替表現はない
if enabled: { start(); log(); }ただし、単一文なら同じ行へ書けます。
if enabled: start()これはブロック表現の代替ではなく、コロン後の本体が単一のsimple statementだからです。
2. 変数宣言はPascal系、型推論は現代的
var count: int = 10
let name: string = "Alopex"
const maxRetries = 3var
count: Integer;
name: String;
const
MaxRetries = 3;count = 10
name = "Alopex"
MAX_RETRIES = 3Nimの宣言はPascal系で、名前 : 型という順序を採ります。
var count: intC系は逆です。
int count;また、Nimはvar、let、constを区別します。
var mutableValue = 1
let immutableValue = 2
const compileTimeValue = 3| 宣言 | 意味 |
|---|---|
var | 再代入可能な変数 |
let | 初期化後に再代入できない値 |
const | コンパイル時に評価される定数 |
Pythonには言語レベルのletやconstはありません。Pascalにはvarとconstがありますが、NimのletはML、Rust、Swiftなど現代的な静的不変束縛に近いものです。
型推論も備えており、初期値から型が決まります。
var count = 10 # int
var ratio = 0.5 # float
var active = true # boolただしPythonの動的型付けとは異なります。
var value = 10
value = "ten" # コンパイルエラーvalueの型は最初の代入時にintへ決まり、その後も変わりません。公式チュートリアルはこれをローカル型推論と説明しています。Pythonでは同じコードが成立します。
value = 10
value = "ten"NimがPythonと似ているのは宣言を省略できる簡潔さであって、動的型付けまで似ているわけではありません。
3. 代入はPython・C系スタイル、Pascalの:=とは異なる
x = 10x = 10x := 10;Nimは代入に=を使用し、比較は==です。
if x == 10:
echo "ten"これはPython・C・Java系です。Pascalでは次のようになります。
x := 10;
if x = 10 then
WriteLn('ten');変数宣言はPascal風ですが、代入演算子はPascalとは似ていません。
4. 手続きはPascalの語彙、戻り値は式指向
proc add(a, b: int): int =
a + bfunction Add(a, b: Integer): Integer;
begin
Add := a + b;
end;def add(a: int, b: int) -> int:
return a + bNimは通常の関数をfunctionやdefではなくprocと呼びます。これはPascal/Modula系のprocedureを思わせる語彙です。
ただし戻り値の扱いはPascalより現代的で、末尾の式が戻り値になります。明示的にresultへ代入する方法もあります。
proc add(a, b: int): int =
result = a + b早期リターンも可能です。
proc classify(value: int): string =
if value < 0:
return "negative"
if value == 0:
return "zero"
result = "positive"Pascalの伝統的な「関数名へ代入する」方式(Add := a + b;)ではありません。Nimのresultは暗黙の戻り値変数です。構文上はPascal系の手続き宣言に近い一方、末尾式を値とする点はRust、Scala、ML系に近い式指向です。
5. 同じ型の引数をまとめる記法はPascalそのものに近い
proc distance(x1, y1, x2, y2: float): float =
discardこれは4引数すべてがfloatです。Pascalにも同じ形式があります。
function Distance(x1, y1, x2, y2: Real): Real;Pythonでは通常、個別に型注釈を書きます。
def distance(
x1: float,
y1: float,
x2: float,
y2: float
) -> float:
...Nimでは異なる型をセミコロンで区切る書き方もあります。
proc connect(host: string; port: int; secure: bool) =
discardカンマでも記述できます。
proc connect(host: string, port: int, secure: bool) =
discardセミコロンを型グループの区切りとして使う点はPascalの手続き宣言に近い設計です。
6. ifはPython風だが、式としても使える
let label =
if score >= 80:
"excellent"
elif score >= 60:
"pass"
else:
"fail"Pythonの通常のif文は値を返しません。
if score >= 80:
label = "excellent"
elif score >= 60:
label = "pass"
else:
label = "fail"Pythonで式にする場合は条件式を使います。
label = "excellent" if score >= 80 else "pass"Nimではブロック形式のif全体が式になれます。
proc absolute(x: int): int =
if x >= 0:
x
else:
-xこれはPythonよりもRust、Scala、Kotlin、ML系に近い特徴です。Nimは手続き型言語として書けますが、多くの構文が値を生成できます。「文中心のPascal」と「式中心の関数型言語」の中間にあります。
7. caseはPascal風だが、より強い
case status
of 200:
echo "OK"
of 400, 404:
echo "client error"
of 500..599:
echo "server error"
else:
echo "unknown"case Status of
200:
WriteLn('OK');
400, 404:
WriteLn('client error');
500..599:
WriteLn('server error');
else
WriteLn('unknown');
end;Nimのcase、of、範囲記号..はPascalとの類似が非常に強い部分です。
Python 3.10以降のmatchは構造パターンマッチングです。
match status:
case 200:
print("OK")
case 400 | 404:
print("client error")Nimの基本的なcaseは代数的データ型に対するML系パターンマッチそのものではありませんが、列挙型、整数、文字列などを静的に分岐でき、網羅性検査も活用できます。
さらにNimではcaseも式になります。
let message =
case status
of 200: "OK"
of 404: "Not Found"
else: "Unknown"この点は従来のPascalより式指向です。
8. 列挙型・部分範囲・集合はPascal系との類似が濃い
列挙型:
type
Status = enum
pending
running
completed
failed部分範囲型:
type
Percentage = range[0..100]
var progress: Percentage = 80集合型:
type
Permission = enum
read
write
execute
let adminPermissions = {read, write, execute}
if write in adminPermissions:
echo "writable"Pascalにも列挙型、部分範囲型、集合型があります。
type
TPercentage = 0..100;
TPermission = (ReadPerm, WritePerm, ExecutePerm);
TPermissions = set of TPermission;これらはPythonの中核的な型機能ではありません。Pythonではenum.Enum、set、バリデーションを別々に組み合わせます。Nimではこれらが言語の静的型システムへ統合されています。公式チュートリアルも列挙型、順序型、部分範囲、集合を基本的な高度型として扱っています。
特にrange[0..100]は、Pascal的な「値域を型として表す」考え方です。
9. 配列はPascal的、シーケンスはPython的
固定長配列:
var values: array[0..4, int]
values[0] = 10配列のインデックス型を明示できます。
type
Weekday = enum
monday, tuesday, wednesday, thursday, friday
var hours: array[Weekday, int]
hours[monday] = 8これはCの配列よりPascalの配列に近い設計です。
type
TWeekday = (Monday, Tuesday, Wednesday, Thursday, Friday);
THours = array[TWeekday] of Integer;動的シーケンス:
var values: seq[int] = @[10, 20, 30]
values.add 40seq[T]は可変長の連続コンテナです。
let names = @["Alice", "Bob", "Carol"]利用感はPythonのlistに近いものです。
names = ["Alice", "Bob", "Carol"]ただしNimのシーケンスは要素型が固定されます。
var values = @[1, 2, 3]
values.add "four" # コンパイルエラーまとめると、
- 固定長・インデックス型付き配列: Pascal系
- リテラルで簡単に作れる可変長コンテナ: Python的利便性
- 要素型は静的: Nimの静的型システム
という組み合わせです。
10. オブジェクトはPascalのrecordとCのstructに近い
type
Person = object
name: string
age: int
let person = Person(name: "Alice", age: 30)Pascalではrecordです。
type
TPerson = record
Name: String;
Age: Integer;
end;Pythonではクラスまたはdataclassを使います。
from dataclasses import dataclass
@dataclass
class Person:
name: str
age: int重要なのは、Nimのobjectがデフォルトでは値型であることです。
var a = Person(name: "Alice", age: 30)
var b = a
b.name = "Bob"
echo a.name # Alice
echo b.name # BobこれはPythonの通常のクラスインスタンスとは異なります。Pythonでは変数代入によって同じオブジェクトを参照します。
a = Person("Alice", 30)
b = a
b.name = "Bob"
print(a.name) # Bob参照型にしたい場合、Nimでは明示的にref objectを使います。
type
PersonRef = ref object
name: string
age: intこの「値か参照かを型で明示する」設計は、PythonよりC++、Pascal、Rustに近いものです。
11. オブジェクト継承はあるが、クラス中心ではない
type
Person = ref object of RootObj
name: string
Employee = ref object of Person
employeeId: int継承可能な基底型には通常RootObjを使用します。
method describe(person: Person): string {.base.} =
"person: " & person.name
method describe(employee: Employee): string =
"employee: " & employee.nameNimはprocとmethodを区別します。
proc: 静的ディスパッチmethod: 動的ディスパッチ
Pythonのメソッドは通常、クラス定義内に置かれます。
class Person:
def describe(self):
return f"person: {self.name}"Nimでは型と操作を分離して書くことが一般的です。
type
Person = object
name: string
proc describe(person: Person): string =
"person: " & person.nameこれはPascalのrecord+外部procedure、Cのstruct+関数、Rustの型とimplの考え方に近く、PythonやJavaの「すべてをクラスへ格納する」構造とは異なります。公式チュートリアルも、NimのOOPは意図的に最小限であり、OOPを唯一の設計方法とはしていないと説明しています。
12. メソッド呼び出し記法はUFCS
次の2つは同じ意味です。
add(values, 10)
values.add(10)文字列操作でも同様です。
let normalized = strip(toLowerAscii(name))は、次のように書けます。
let normalized = name.toLowerAscii.stripこの機能は一般にUFCS(Uniform Function Call Syntax)と呼ばれます。
Pythonでは、
name.lower().strip()のように見えますが、意味は異なります。Pythonではlowerとstripは基本的にオブジェクトに所属するメソッドです。Nimでは、
name.strip()が概念的には、
strip(name)へ変換されます。このためデータ型と関数を疎結合に保ちながら、メソッドチェーンのように記述できます。
let result =
data
.filterIt(it > 0)
.mapIt(it * 2)見た目はPythonやJavaScriptのメソッドチェーンですが、基礎は自由関数です。Nimはオブジェクト指向の見た目を、手続き型の関数モデルの上に提供する言語です。
13. 識別子は大文字小文字を区別するが、アンダースコアを無視する
Nimには独特な名前比較規則があります。概念的には、次の名前は同一視されます。
parseHttpHeader
parse_http_header先頭文字を除き大文字小文字も正規化されるため、Nimのスタイル非依存性は強力です。標準的にはcamelCaseを使います。
proc parseHttpHeader() =
discard呼び出し側は次のようにも書けます。
parse_http_header()一方、先頭文字の大文字・小文字には意味があります。
Foo
fooこれは別の識別子です。
この規則はPython、Pascal、Cのどれにもそのまま対応しません。Nim独自の「API表記の差異を吸収する」設計です。利点は、外部ライブラリやC APIをNim流の名前で扱いやすいことです。一方で、名前の表記揺れをコンパイラが許容するため、検索や規約の観点では評価が分かれます。
14. forはPython風イテレーションだが、イテレータは静的に展開される
for value in values:
echo valueこれはPythonとほぼ同じ外観です。
for value in values:
print(value)範囲は次のように書きます。
for i in 0..5:
echo i0..5は5を含みます(0, 1, 2, 3, 4, 5)。Pythonでは次に相当します。
for i in range(0, 6):
print(i)終端を含まない範囲には..<を使います。
for i in 0..<5:
echo i結果は0, 1, 2, 3, 4です。
独自イテレータも定義できます。
iterator countdown(start: int): int =
var current = start
while current >= 0:
yield current
dec current
for value in countdown(3):
echo value出力:
3
2
1
0yieldを使う見た目はPythonジェネレータに似ています。
def countdown(start):
current = start
while current >= 0:
yield current
current -= 1ただしNimのインラインイテレータは通常、コンパイラによる展開を前提としたゼロコスト抽象化です。Pythonのジェネレータオブジェクトと同じ実行モデルではありません。yieldはイテレータからfor本体へ制御を戻す構文です。
15. whileとループ制御はPascal・C・Pythonの共通領域
var count = 0
while count < 10:
inc count
if count == 5:
continue
if count == 8:
breakwhile、break、continueはPythonやC系と同じです。Pascalには伝統的にcontinueがなかった処理系もありますが、現代のFree PascalやDelphiでは利用できます。
Nim独自に便利なのが**ラベル付きblock**です。
block search:
for row in matrix:
for value in row:
if value == target:
echo "found"
break search多重ループを一度に抜けられます。Pythonではフラグ、関数化、例外などが必要になる場面です。
found = False
for row in matrix:
for value in row:
if value == target:
found = True
break
if found:
breakNimのblockは、Pascalの名前付きブロックというより、Adaや一部の構造化言語にあるラベル付き脱出に近い機能です。
16. 例外構文はPython風とPascal風の混成
try:
riskyOperation()
except IOError as error:
echo error.msg
except ValueError:
echo "invalid value"
finally:
cleanup()Pythonと非常によく似ています。
try:
risky_operation()
except OSError as error:
print(error)
except ValueError:
print("invalid value")
finally:
cleanup()一方、Nimの例外型は次のように定義します。
type
ConfigurationError = object of CatchableError送出する場合:
raise newException(ConfigurationError, "invalid configuration")Pythonでは:
class ConfigurationError(Exception):
pass
raise ConfigurationError("invalid configuration")構文の外観はPython寄りですが、例外型の静的階層やeffect trackingはPythonより厳密です。手続きが送出する例外をpragmaで記述できます。
proc loadConfig(): string {.raises: [IOError].} =
readFile("config.toml")このeffect annotationは、Javaのchecked exceptionそのものではありませんが、Pythonにはない静的な副作用情報です。
17. deferはGo・Swift・D系に近い
let file = open("data.txt")
defer:
file.close()
let content = file.readAll()現在のスコープを抜けるとfile.close()が実行されます。
Pythonでは通常withを使います。
with open("data.txt") as file:
content = file.read()Pascalではtry...finallyが一般的です。
FileStream := TFileStream.Create('data.txt', fmOpenRead);
try
...
finally
FileStream.Free;
end;Nimにもtry/finallyはありますが、deferによって資源解放を取得位置の近くに書けます。
let lock = acquireLock()
defer: releaseLock(lock)これはPythonやPascalではなく、Go、D、Swiftなどに見られるスタイルです。
18. ジェネリクスはC++より簡潔で、ML系の型制約も持つ
proc first[T](values: openArray[T]): T =
values[0]
echo first([1, 2, 3])
echo first(["a", "b", "c"])明示的にも呼べます。
echo first[int]([1, 2, 3])型制約:
proc double[T: SomeNumber](value: T): T =
value * 2複数型:
proc convert[T, U](value: T; converter: proc(x: T): U): U =
converter(value)Nimのジェネリクスはコンパイル時に具体化される点ではC++テンプレートやRustジェネリクスに近いものです。型パラメータを[]で書く点はAdaや一部のPascal系ジェネリクスより、C++・C#・Scalaなどに近い構文です。
Nimにはconceptもあります。
type
Addable = concept x, y
x + y
proc combine[T: Addable](a, b: T): T =
a + bこれはGoのinterface、Rustのtrait bound、C++20 concepts、Haskellのtype classに類似する構造的制約です。ただしそれらと完全に同一ではありません。
19. distinct型はPascalの型安全性をさらに進めたもの
type
UserId = distinct int
ProductId = distinct int
let userId = UserId(10)
let productId = ProductId(10)基礎表現は両方intですが、異なる型です。
proc loadUser(id: UserId) =
discard
loadUser(userId)
loadUser(productId) # コンパイルエラーPythonの型エイリアスは通常、実行時にも静的型検査上にも完全な別型とは限りません。
UserId = int
ProductId = intPascalには型宣言による区別がありますが、処理系や宣言形式によってaliasとして扱われる場合があります。Nimのdistinctは、同じ表現を持ちながら混同を防ぐための明示的なnominal typeです。
type
Meter = distinct float
Second = distinct float単位の取り違え防止にも使えます。
20. Variant ObjectはPascalのvariant recordと代数的データ型の中間
type
NodeKind = enum
integerNode
stringNode
Node = object
case kind: NodeKind
of integerNode:
intValue: int
of stringNode:
stringValue: string利用例:
let number = Node(kind: integerNode, intValue: 42)
let text = Node(kind: stringNode, stringValue: "hello")
case number.kind
of integerNode:
echo number.intValue
of stringNode:
echo number.stringValuePascalにもvariant recordがあります。
type
TNode = record
case Kind: TNodeKind of
nkInteger: (IntValue: Integer);
nkString: (StringValue: String);
end;一方、Rustならenumの各variantに値を持たせます。
enum Node {
Integer(i32),
String(String),
}Nimのvariant objectはPascalのvariant recordに構文的に非常に近い一方、判別子とフィールドの対応を型安全に扱うため、実用上は代数的データ型に近い使い方ができます。Nimコンパイラ公式のAST型そのものも、case kindを持つvariant objectとして説明されています。
21. ポインタと参照を区別する点はPythonとは根本的に違う
type
Node = ref object
value: int
next: Noderefは管理された参照です。
var node = Node(value: 1)一方ptrは低レベルポインタです。
var value = 10
var pointer: ptr int = addr value
echo pointer[]ref T: Nimのメモリ管理下にある参照ptr T: Cに近い非管理ポインタT: 値そのもの
Pythonではこれらの違いが表面構文にほぼ現れません。Pascalでは値、クラス参照、ポインタを区別します。C/C++では値とポインタを区別します。この点でNimは明確にシステム言語側です。
22. 演算子は通常の手続きとして定義できる
type
Vector = object
x, y: float
proc `+`(a, b: Vector): Vector =
Vector(x: a.x + b.x, y: a.y + b.y)
let a = Vector(x: 1, y: 2)
let b = Vector(x: 3, y: 4)
let c = a + bバッククォートによって演算子名を識別子として扱います。独自演算子も定義できます。
proc `***`(a, b: int): int =
a * a + b * b
echo 3 *** 4Pythonにも演算子オーバーロードがありますが、__add__などの特殊メソッドを使います。
def __add__(self, other):
...C++はoperator+です。Nimでは演算子が特別なメソッドではなく、通常のオーバーロード可能な手続きとして扱われます。この統一性は関数型言語やLisp系の発想にも近い部分です。
23. コマンド呼び出し構文はシェル・DSL的
通常の関数呼び出し:
echo("hello", " ", "world")括弧を省略できます。
echo "hello", " ", "world"さらにUFCSと組み合わせられます。
"hello".echoこの構文はPythonにはありません(print("hello")と書くしかありません)。Pascalでは括弧を省略できる場合もありますが、Nimほど一般化されていません。
Nimでは次のようなDSL的記述が成立します。
window:
title "Settings"
width 800
height 600これは通常、templateやmacroと組み合わせて実現します。
ただし、括弧省略は優先順位が分かりにくくなることがあります。
echo abs -10このようなコードでは、どこまでが引数かを理解する必要があります。複雑な式では括弧を明示したほうが安全です。
24. テンプレートはCプリプロセッサより型安全で、関数より早い段階で展開される
template twice(body: untyped) =
body
body
twice:
echo "hello"展開後の概念:
echo "hello"
echo "hello"式を受け取る例:
template withLock(lock; body: untyped) =
acquire(lock)
try:
body
finally:
release(lock)
withLock myLock:
updateSharedState()これは構文として新しい制御構造のように見えます。Cのマクロ:
#define TWICE(x) do { x; x; } while (0)よりもAST・スコープ・型情報に統合されています。
Pythonのデコレータや高階関数では、同じ構文拡張はできません。
with lock:
update_shared_state()Pythonのwith自体は言語組み込み構文であり、利用者が新しい文法的ブロックを自由に作るわけではありません。Nimではtemplateによって、ライブラリ側が言語構文に近いAPIを提供できます。
25. マクロはLisp的だが、表面構文は通常のNim
import std/macros
macro debug(expression: untyped): untyped =
result = quote do:
echo `expression`.astToStr, " = ", `expression`
let value = 42
debug valueマクロはコンパイル時にNimのASTを受け取り、別のASTを返します。公式チュートリアルも、マクロを「コンパイル時に実行され、Nimの構文木を別の構文木へ変換する関数」と定義しています。
Lispの(defmacro ...)、Rustのmacro_rules! ...、Elixirのdefmacro ...などと同じく、コードをデータとして扱うメタプログラミングです。
ただしLispではコード自体がS式ですが、Nimでは通常のインデント構文をASTへ変換します。
dumpTree:
var person = Person(name: "Alice", age: 30)dumpTreeによって構文木を確認できます。公式マクロチュートリアルは、コードブロックをマクロの末尾引数として渡せること、dumpTreeでAST表現を調べられることを説明しています。
このためNimは、Python風の読みやすい表面構文の下に、Lisp級の構文操作機構を持つと評価できます。
26. コンパイル時実行はC++ constexprより広範囲
func factorial(n: int): int =
if n <= 1:
1
else:
n * factorial(n - 1)
const value = factorial(10)factorial(10)はコンパイル時に計算されます。
明示的なコンパイル時ブロック:
static:
echo "executed during compilation"コンパイル時条件:
when defined(windows):
echo "Windows"
elif defined(linux):
echo "Linux"
else:
echo "other"whenは実行時のifではありません。選ばれなかった分岐は通常、コンパイル対象になりません。
when sizeof(int) == 8:
type NativeWord = int64
else:
type NativeWord = int32Nimの公式マニュアルは、定数式、マクロ定義、マクロから呼ばれるNim手続きなどをコンパイル時に実行でき、意味解析とコンパイル時実行が相互に進行すると説明しています。これは、
- Cのプリプロセッサ
- C++の
constexpr - DのCTFE
- Zigの
comptime - Lispのマクロ
の要素を統合したような領域です。
27. PragmaはPascalのdirectiveとCのattributeの中間
proc importedFunction(x: cint): cint
{.importc, header: "<example.h>".}proc fastOperation() {.inline.} =
discardtype
PackedHeader {.packed.} = object
version: uint8
flags: uint8Pragmaは{.と.}で囲みます。公式チュートリアルは、pragmaを大量の予約語を増やさず、コンパイラへ追加情報や命令を与える仕組みと説明しています。
Pascalのコンパイラdirective:
{$IFDEF WINDOWS}C/C++のattribute:
[[nodiscard]]Rustのattribute:
#[derive(Debug)]に近い役割です。ただしNimではFFI、最適化、effect、メモリ表現、エクスポートなど、多くの言語機能をpragmaへ統一しています。
28. モジュールの公開記号は*で明示する
type
Person* = object
name*: string
age: int
proc newPerson*(name: string; age: int): Person =
Person(name: name, age: age)*が付いた記号だけが他モジュールへ公開されます。
Person*: 公開name*: 公開age: 非公開newPerson*: 公開
Pythonでは先頭アンダースコアが慣習です。
_internal_value = 10Pascalではinterfaceとimplementationのセクションで公開範囲を分けます。Nimは宣言単位で公開性を付与します。Rustのpubを後置記号へ圧縮したような設計です。
pub struct Person {
pub name: String,
age: u32,
}この*は短い一方、ポインタ演算子と誤認しやすいですが、Nimでは公開マーカーです。
29. import構文はPython風だが、選択的公開制御が強い
import std/strutils複数:
import std/[strutils, sequtils, tables]特定記号:
from std/strutils import strip, toLowerAscii別名:
import std/strutils as strings除外:
import std/strutils except splitPython:
import pathlib
from pathlib import Pathに似ています。Pascalのusesより柔軟です。
uses
SysUtils, Classes;Nimではモジュールパス、選択import、除外、aliasが言語レベルでまとまっています。
30. コメントはPython風、ネスト可能な複数行コメントは独自性が強い
# 一行コメントPythonと同じです。
ドキュメントコメント:
proc add(a, b: int): int =
## 2つの整数を加算する。
a + b複数行コメント:
#[
複数行コメント
]#さらにネストできます。
#[
外側
#[
内側
]#
外側の続き
]#公式マニュアルは#[ ... ]#形式の複数行コメントと、そのネストを明示しています。Cの/* ... */は標準ではネストできません。Pythonには専用の複数行コメント構文がありません。Pascalの{ ... }や(* ... *)は処理系によってネスト規則が異なります。
31. 文字列はPython的な利便性に、システム言語向け表現を加えている
通常文字列:
let message = "hello"複数行文字列:
let sql = """
select *
from users
where active = true
"""raw string:
let path = r"C:\Users\name\data"汎用raw文字列:
let regex = re"\d+\.\d+"re"..."のreは特殊な文法キーワードではなく、文字列リテラルに対する一般化された接頭辞呼び出しです。
文字列補間は標準ライブラリのマクロで実現されます。
import std/strformat
let name = "Alice"
let age = 30
echo fmt"{name} is {age} years old"Python:
print(f"{name} is {age} years old")見た目は似ていますが、Nimのfmtは言語組み込みではなく、コンパイル時マクロです。公式マクロチュートリアルもstrformatを実用的なマクロ例として挙げています。
ここにNimの設計思想が現れています。便利な構文をすべてコンパイラへハードコードせず、マクロによってライブラリ側で実装できるようにしています。
32. 型変換はPython風の呼び出し記法だが、暗黙変換は制限的
let integerValue = int(3.14)
let floatValue = float(10)見た目はPythonです。
integer_value = int(3.14)
float_value = float(10)ただしNimは静的型付きなので、変換の可否はコンパイル時に検査されます。
型キャストもあります。
let bits = cast[uint32](someFloat)通常の型変換(int(value))と低レベルな再解釈(cast[int](value))を分けています。C系のcast、Pascalの型キャスト、Pythonのコンストラクタ風変換を組み合わせた構文です。
33. nilはPythonのNoneではなく、ポインタ的なnull
var node: ref Node = nil
if node.isNil:
echo "no node"Python:
node = None
if node is None:
print("no node")見た目は似ていますが、Nimのnilは参照・ポインタ・一部の手続き型などに対するnull値です。任意の値型へ代入できる「値がない」汎用値ではありません。
安全に値の有無を表すなら、Option[T]を使います。
import std/options
proc findUser(id: int): Option[string] =
if id == 1:
some("Alice")
else:
none(string)これはRustのOption<T>やHaskellのMaybeに近いものです。
34. 型クラス的なオーバーロード解決
proc describe(value: int): string =
"integer: " & $value
proc describe(value: string): string =
"string: " & value
echo describe(42)
echo describe("hello")同名手続きを型でオーバーロードできます。Pythonでは通常、同名関数を再定義すると上書きになります。
def describe(value: int):
...
def describe(value: str):
...標準Pythonでは2つの実装として共存しません。
Pascal、C++、Javaにはオーバーロードがあります。NimはさらにUFCSやジェネリクスと組み合わせます。
echo 42.describe
echo "hello".describeオーバーロード+UFCS+型推論によって、クラス階層を作らずに多相的なAPIを構成できます。
35. funcは副作用制約付きの手続き
func square(value: int): int =
value * valueNimにはprocとは別にfuncがあります。
proc logAndSquare(value: int): int =
echo value
value * valuefuncは副作用を制限した手続きとして使われます。
func logAndSquare(value: int): int =
echo value # 副作用として拒否される
value * valuePythonには言語レベルの純粋関数指定はありません。Haskellほど全面的な純粋関数型ではありませんが、Nimは手続き単位で副作用を制限できます。
Nimは、
- 通常処理:
proc - 副作用を抑えた処理:
func - 値を生成する反復:
iterator - 構文展開:
template - AST変換:
macro
を異なる宣言キーワードとして明確に分けています。この分類はNim固有の重要な特徴です。
36. Nimの構文を一つの例にまとめる
import std/[options, strformat]
type
UserId = distinct int
Role = enum
guest
member
administrator
User = object
id: UserId
name: string
role: Role
proc displayName(user: User): string =
case user.role
of guest:
fmt"Guest {user.name}"
of member:
user.name
of administrator:
fmt"Administrator {user.name}"
proc findUser(id: UserId): Option[User] =
if id == UserId(1):
some User(
id: id,
name: "Alice",
role: administrator
)
else:
none(User)
let userId = UserId(1)
case findUser(userId)
of Some(@user):
echo user.displayName
of None:
echo "User not found"ただし最後のOptionに対するパターン構文はライブラリやNimのバージョン・利用方式によって書き方を確認すべき部分です。より標準的に書くなら次のとおりです。
let found = findUser(userId)
if found.isSome:
let user = found.get
echo user.displayName
else:
echo "User not found"この例に現れる構文がどのスタイルに近いかを整理すると、次のようになります。
| 構文 | 近いスタイル |
|---|---|
| インデントブロック | Python/オフサイドルール |
import | Python的 |
typeセクション | Pascal/Modula的 |
Name: Type | Pascal的 |
distinct int | 強い静的型、Nim独自色 |
enum | Pascal系+現代型言語 |
object | Pascal record/C struct |
proc | Pascal/Modula |
| 末尾式による戻り値 | ML/Rust/Scala的 |
case ... of | Pascal的 |
ifを式として使用 | ML/Rust的 |
Option[T] | ML/Haskell/Rust的 |
user.displayName | UFCS |
fmt"..." | ASTマクロによる拡張 |
「PythonとPascalのどちらに似ているか」の精密な整理
Pythonと似ている特徴
構文上、Pythonとの類似が明確なのは次です。
- インデントでブロックを構成
if/elif/elsefor x in collectionwhiletry/except/finallyraiseimport/from ... import#コメント[]によるコンテナ記法- 簡潔な関数呼び出し
- 高い可読性を優先した表面構文
ただし次はPythonとは似ていません。
- 動的型付け
- duck typingを中心にした実行時設計
- すべてがオブジェクトというモデル
- クラス中心のメソッド設計
- 実行時リフレクション中心のメタプログラミング
- インタプリタを前提とした実行モデル
- Pythonジェネレータと同一のイテレータ実装
つまり、NimはPythonと見た目・操作感が似ているが、実行モデルは似ていないと言えます。
Pascalと似ている特徴
Pascalとの類似が強いのは次です。
var、const、typeprocという手続き型の語彙name: Typeという型注釈- 同型引数をまとめる宣言
case ... ofenum- 部分範囲型
- 集合型
- インデックス型を指定できる配列
- recordに近いobject
- variant recordに近いvariant object
- 値型と参照型の区別
- コンパイル言語
- Cとの近接性を意識した静的なデータ表現
ただし次はPascalとは似ていません。
begin/end- 文末の必須セミコロン
- 代入の
:= - 比較の
= thendo- 関数名への戻り値代入
- 宣言部と実行部を厳格に分離する構造
program ... begin ... end.形式
つまり、NimはPascalの型と宣言のスタイルに似ている一方、Pascalの冗長な区切り記号は持たない構文です。
Nim固有の中心は「Python+Pascal」ではなく拡張可能な文法
NimをPythonとPascalの中間としてだけ理解すると、最も重要な部分を見落とします。Nimの中心は、次の組み合わせです。
インデントベースの統一構文
+
静的型付けと値表現
+
オーバーロードとUFCS
+
template
+
AST macro
+
compile-time execution例えば次のコードは、利用者が作った構文のように見えます。
benchmark "database lookup":
let result = database.find(key)
check result.isSomebenchmarkやcheckがtemplateやmacroであれば、ライブラリが制御構文に近いDSLを提供できます。
Pythonでは新しい構文を追加できないため、通常は関数、デコレータ、context managerへ寄せます。
with benchmark("database lookup"):
result = database.find(key)
assert result is not NonePascalではさらに文法的な制約が強くなります。Nimは表面上の構文を小さく保ち、その上でライブラリがASTを操作して領域固有構文を作る設計です。公式資料でもマクロがコンパイル時にASTを変換し、任意のコードブロックを引数として受け取れることが明示されています。
まとめ
本稿の要点は次のとおりです。
Nimを一文で表すなら、次の説明が最も正確です。
Pascal系の宣言・型・値表現を、Python系のインデント構文で記述し、C系の低レベル制御、ML系の式指向、Lisp系のASTメタプログラミングを統合した言語。
本稿の分類は「どの言語から歴史的に採用したか」という由来ではなく、公式仕様に基づく「構文・意味論がどれだけ似ているか」の比較です。コーディングする側にとって重要なのは由来ではなく、どのようなスタイルの構文か、だからです。
Nimの本質は、Pythonのように書けることよりも、静的な意味を失わずに、構文上の摩擦だけを減らしたことにあります。Pythonがインデントによって動的言語を簡潔にしたのに対し、NimはインデントによってPascal・C級の型とシステム制御を簡潔にしています。そしてNimの中心はtemplate・ASTマクロ・コンパイル時実行による拡張可能な文法にあり、「Python+Pascal」という理解だけでは最も重要な部分を見落とします。
前提として確認した各言語のブロック設計は次のとおりです。PascalとPythonの最大の違いはブロックの表現方法であり、Pascalのbegin/endはトークンでブロックを作り、インデントはコンパイラに無視される「人間のための整形」にすぎません。一方Pythonのインデントは字句解析段階でINDENT/DEDENTトークンへ変換される「構文記号」です。この「インデントを構文にする」設計はオフサイドルールと呼ばれ、Haskell、F#、Elm、CoffeeScript、Nimなどが採用していますが、設計は一枚岩ではありません。Haskellではレイアウトルールは{}と;の糖衣構文であり明示記法へ切り替えられますが、Pythonはインデントしかブロック表現を持たず、Nimもタブ禁止・{}代替なしという独自の立て付けです。
Nimそのものの入門・実性能については過去記事「「Nim」入門 — Cにトランスパイルするマルチパラダイム言語の現在地」もあわせて参照してください。