asopi tech
asopi techIndie Developer
Nimの構文・ディープインサイト ― Pascal・Python・Haskellとどこが似ているのか

【2026年7月版】

Nimの構文・ディープインサイト ― Pascal・Python・Haskellとどこが似ているのか

公開日: 2026/07/24
読了時間: 約 57分

本稿の範囲

本稿の主題は、Nimの各構文がPascal・Python・C系・関数型言語のどのスタイルにどう似ているかの全域比較です(第5部、全38トピック)。ブロック構文だけでなく、宣言・型システム・式指向・ジェネリクス・メタプログラミング・コンパイル時実行までを対象とします。「どの言語から歴史的に採用したか」という由来には立ち入りません。コーディングする側にとって重要なのは、どのようなスタイルの構文か、だからです。

その前提として、比較対象となる言語の設計を先に整理します。

  1. PascalとPythonの構文比較(第1部)
  2. 「Pascalのインデントは整形、Pythonのインデントは構文」という違い(第2部)
  3. インデントがブロックになる言語(オフサイドルール採用言語)の概観(第3部)
  4. Haskellのレイアウトルールが糖衣構文であること(第4部)
  5. Nim構文のスタイル比較(第5部)

Nimのパートでは「見た目が似ている」と「意味論まで似ている」を区別しながら、具体的なコード例をもとに構文ごとに整理します。Nimに関する記述は公式マニュアル・公式チュートリアル上のNim 2.2.10時点の仕様に基づきます。

第1部 PascalとPythonの構文比較

PascalとPythonはどちらも「教育向け」として広く使われた言語ですが、設計思想は異なります。Pascalはアルゴリズムを厳密に記述することを重視し、Pythonは読みやすさと生産性を重視しています。

代表的な構文の対応は次のとおりです。

内容PascalPython
コメント{...} または (*...*)#
変数宣言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 + b

Pascalでは関数名に代入することで戻り値を返します(DelphiではResult := ...も利用可能)。この方式は後述するNimのresult変数と対比されます。

配列

var
  a: array[1..5] of Integer;
a[1] := 10;
a = [0] * 5
a[0] = 10

Pascalは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")

構文上の大きな違い

PascalPython
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のコーディング規約では改行スタイルが推奨されています。beginendの対応関係が見やすくなり、ネストしたブロックも追いやすくなるためです。

条件分岐でも同様です。

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)の段階でINDENTDEDENTというトークンに変換されます。Pythonにとってインデントは括弧やbegin/endと同じ「構文記号」です。

その意味では、Pythonの構文はPascalよりも、Lispの括弧やC言語の{}に近い役割をインデントが担っていると言えます。これは「見た目の違い」ではなく、「ブロックを何で表現するか」という言語設計の根本的な違いです。

第3部 インデントがブロックになる言語たち — オフサイドルール

Python以外にも、インデントそのものがブロックになる言語は存在します。ただし、Pythonほど普及しているものは多くありません。

Haskell

main = do
    print "Hello"

foo x =
    if x > 0
        then x
        else -x

Haskellは**レイアウトルール(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 * x

JavaScriptへコンパイルされる言語で、Python風のインデント構文を採用しています。

Cobra

Python風構文を持つ.NET言語です。

共通する考え方

これらの言語は「Off-side Rule(オフサイドルール)」または「Layout Rule(レイアウトルール)」を採用しています。仕組みは次のとおりです。

  • 行頭位置をLexerが追跡する
  • インデントが増えたらブロック開始
  • インデントが減ったらブロック終了

PythonのINDENT/DEDENTもこの考え方に基づいています。

Pascalとの関係で特筆すべきNim

この中で特筆すべきはNimです。Nimは、

  • 静的型付け
  • vartypeprocなど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 = 3
var
  count: Integer;
  name: String;

const
  MaxRetries = 3;
count = 10
name = "Alopex"
MAX_RETRIES = 3

Nimの宣言はPascal系で、名前 : 型という順序を採ります。

var count: int

C系は逆です。

int count;

また、Nimはvarletconstを区別します。

var mutableValue = 1
let immutableValue = 2
const compileTimeValue = 3
宣言意味
var再代入可能な変数
let初期化後に再代入できない値
constコンパイル時に評価される定数

Pythonには言語レベルのletconstはありません。Pascalにはvarconstがありますが、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 = 10
x = 10
x := 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 + b
function Add(a, b: Integer): Integer;
begin
  Add := a + b;
end;
def add(a: int, b: int) -> int:
    return a + b

Nimは通常の関数をfunctiondefではなく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のcaseof、範囲記号..は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.Enumset、バリデーションを別々に組み合わせます。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 40

seq[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.name

Nimはprocmethodを区別します。

  • 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ではlowerstripは基本的にオブジェクトに所属するメソッドです。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 i

0..55を含みます(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
0

yieldを使う見た目は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:
    break

whilebreakcontinueは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:
        break

Nimの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 = int

Pascalには型宣言による区別がありますが、処理系や宣言形式によって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.stringValue

Pascalにも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: Node

refは管理された参照です。

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 *** 4

Pythonにも演算子オーバーロードがありますが、__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 = int32

Nimの公式マニュアルは、定数式、マクロ定義、マクロから呼ばれる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.} =
  discard
type
  PackedHeader {.packed.} = object
    version: uint8
    flags: uint8

Pragmaは{..}で囲みます。公式チュートリアルは、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 = 10

Pascalではinterfaceimplementationのセクションで公開範囲を分けます。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 split

Python:

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 * value

Nimにはprocとは別にfuncがあります。

proc logAndSquare(value: int): int =
  echo value
  value * value

funcは副作用を制限した手続きとして使われます。

func logAndSquare(value: int): int =
  echo value   # 副作用として拒否される
  value * value

Pythonには言語レベルの純粋関数指定はありません。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/オフサイドルール
importPython的
typeセクションPascal/Modula的
Name: TypePascal的
distinct int強い静的型、Nim独自色
enumPascal系+現代型言語
objectPascal record/C struct
procPascal/Modula
末尾式による戻り値ML/Rust/Scala的
case ... ofPascal的
ifを式として使用ML/Rust的
Option[T]ML/Haskell/Rust的
user.displayNameUFCS
fmt"..."ASTマクロによる拡張

「PythonとPascalのどちらに似ているか」の精密な整理

Pythonと似ている特徴

構文上、Pythonとの類似が明確なのは次です。

  • インデントでブロックを構成
  • if/elif/else
  • for x in collection
  • while
  • try/except/finally
  • raise
  • import/from ... import
  • #コメント
  • []によるコンテナ記法
  • 簡潔な関数呼び出し
  • 高い可読性を優先した表面構文

ただし次はPythonとは似ていません

  • 動的型付け
  • duck typingを中心にした実行時設計
  • すべてがオブジェクトというモデル
  • クラス中心のメソッド設計
  • 実行時リフレクション中心のメタプログラミング
  • インタプリタを前提とした実行モデル
  • Pythonジェネレータと同一のイテレータ実装

つまり、NimはPythonと見た目・操作感が似ているが、実行モデルは似ていないと言えます。

Pascalと似ている特徴

Pascalとの類似が強いのは次です。

  • varconsttype
  • procという手続き型の語彙
  • name: Typeという型注釈
  • 同型引数をまとめる宣言
  • case ... of
  • enum
  • 部分範囲型
  • 集合型
  • インデックス型を指定できる配列
  • recordに近いobject
  • variant recordに近いvariant object
  • 値型と参照型の区別
  • コンパイル言語
  • Cとの近接性を意識した静的なデータ表現

ただし次はPascalとは似ていません

  • begin/end
  • 文末の必須セミコロン
  • 代入の:=
  • 比較の=
  • then
  • do
  • 関数名への戻り値代入
  • 宣言部と実行部を厳格に分離する構造
  • 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.isSome

benchmarkcheckがtemplateやmacroであれば、ライブラリが制御構文に近いDSLを提供できます。

Pythonでは新しい構文を追加できないため、通常は関数、デコレータ、context managerへ寄せます。

with benchmark("database lookup"):
    result = database.find(key)
    assert result is not None

Pascalではさらに文法的な制約が強くなります。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にトランスパイルするマルチパラダイム言語の現在地」もあわせて参照してください。