tutorial
Nim

Nim Tutorial Part Iを日本語訳してみた(前編)

チュートリアルの訳の前に

Nimという言語は、python風の構文を持ち、C言語並みの速度を持つ、プログラミング言語です。
つまり、Pythonistaにとって夢のような言語です。

インストールはこちらがわかりやすいと思います。
公式のインストールページはこちらからどうぞ。

Nimを勉強するにあたり、その日本語文書の少なさがボトルネックになると思い、手始めに公式チュートリアルを翻訳しました。原文はこちらからこれはその前半(パート1)の前編です。
もし、より適したアップロード場所や公式の翻訳フォームがあれば教えてください。

誤訳や誤解にお気づきの際は、遠慮なくお申し付けください。ツイッターにリプを飛ばすと反応は速いです。
サンプルコードは基本的に、ideoneで実行し、リンクを貼るつもりですが、ideoneのNimのバージョンが0.11.2(2016/12/29日現在)ですので、一部手元の実行環境で確認するにとどめています。

Nimチュートリアル パート1 Nim Tutorial (Part I)

著者: Andreas Rumpf
バージョン: 0.15.2

はじめに Introduction

"Der Mensch ist doch ein Augentier -- schöne Dinge wünsch ich mir."
「人間は視覚に頼る動物だ。私は美しい物が欲しい。」1

これはプログラミング言語『Nim』のチュートリアルである。
変数・型・文などの、プログラミングの基本的な概念を理解した読者を想定したチュートリアルではあるものの、かなり基本的な事項のみ扱う。
より発展的な言語仕様の例はマニュアルに多く記載されている。
このチュートリアルや、他のNimの文書に記載されているNimのコード例は全て、Nimスタイルガイドに従う。

最初のプログラム The first program

"hello world"に比べたら複雑なコードでNimの旅を始めよう。

greetings.nim

# これはコメントである
echo "お名前は? "
var name: string = readLine(stdin)
echo "やあ、", name, "!" 

このコードを"greetings.nim"に保存、コンパイル、実行してみよう。

nim compile --run greetings.nim

--runスイッチを付けると、コンパイルが終わるとファイルが自動的に実行される。

コマンドライン引数は、ファイル名の後に記述する事でプログラムに渡せる。

nim compile --run greetings.nim arg1 arg2

よく使われるコマンドやスイッチは、省略形を持ち、以下のような記述も可能である。

nim c -r greetings.nim

release版をコンパイルするには、以下のように記述すれば良い。

nim c -d:release greetings.nim

デフォルト状態ではデバッグの為に、膨大な量のランタイムチェックを作る。2
-d:releaseにより、これらのチェックは無効化され、最適化が行われる
これが何のプログラムかは明白だろうが、構文の説明をしよう。
インデントのない文はプログラム実行時に実行される。
インデントにより、Nimは複数の文を一つにまとめる。
インデントはスペースのみであり、タブキーによるインデントは受け付けない。

文字列リテラルは二重引用符で囲む。
var文により、nameという名前のstring型(文字列型)変数を宣言できる。
この変数にはreadLineプロシージャの返り値が代入されている。
このコードの場合、型の指定を宣言から省いてもよい。
というのも、コンパイラが「readLineプロシージャが文字列型を返す事」を把握しているからである。(これはローカル型推論と呼ばれる。)
よって以下の記述も可能である。

greetings1.nim

var name = readLine(stdin)

Nimにおける型推定が基本的に、このローカル型推定である事を覚えておいてほしい。これは簡潔さと可読性とが歩み寄った結果である。
echoreadLineといった、コンパイラが既に把握している識別子がこの"hello world"コードには含まれている。
これらの組み込まれた識別子は、システムモジュールで宣言されており、これは他の全てのモジュールが暗黙にインポートする。3

字句要素(リテラルや識別子のこと) Lexical elements

Nimの字句要素を詳しく見てみよう。
他のプログラミング言語と同じくNimを構成する要素は、(文字列)リテラル、識別子、キーワード、コメント、演算子、その他記号である。

文字列リテラル、文字リテラルString and character literals

文字列リテラルは二重引用符"で囲まれる。一方の文字リテラルは単引用符'で囲まれる。
特殊文字をエスケープするには\を用いる。例えば、\nは改行、\tはタブを意味する。
raw文字列リテラルもある。4
rawstring.nim

r"C:\program files\nim"

raw文字列リテラル中の\はエスケープ文字ではない。

文字列リテラルを記述する最後の方法は、long文字列リテラルという第三の方法である。
long文字列リテラルは3つの二重引用符に挟まれる。""" ... """
複数行にわたって文字列を記述でき、\もエスケープ文字にならない。

使用例としては、HTMLコードを埋め込む、などがある。5

コメント Comments

文字列リテラル・文字リテラルの中でなければ、コメントは#で記述できる。
ドキュメント・コメントは##が先頭に付く。

comments.nim

# コメント

var myVariable: int ## ドキュメントコメント

ドキュメント・コメントは特別な存在である。
ドキュメント・コメントは抽象構文木6の中に含まれるため、.nimファイル中7の決まった場所にしか記述できないのだ。
これにより、シンプルなドキュメンテーション・ツールで事足りる。
discard文8とlong文字列リテラルを組み合わせて使うことで、ブロックコメントを書く、という方法もある。
discard_and_longstring.nim9

discard """この中ならインデントの心配をすることなく
どんなコードもコメントアウトできる。
      yes("つまらない質問をしてもよろしいですか?")"""

数値 Numbers

数値リテラルの書き方は、他の多くの言語と同じである。
また、アンダーバー_を使って可読性を上げることもできる。(100万:1_000_00010
小数点.eを含む数字は浮動小数点型になる。(10億:1.0e9
16進数は0x、2進数は0b、8進数は0oがそれぞれ先頭に付く。先頭に0を付けたとしても、8進数には出来ない。

var文 The var statement

var文を使うことで、ローカル変数やグローバル変数を新しく宣言できる。

var.nim

var x, y: int # xとyが``int``型であることの宣言

var文の後にインデントを入れると、ひとまとまりの変数を並べて宣言できる。

var_at_once.nim

var
  x, y: int
  # ここにコメントを入れることも可能である
  a, b, c: string

代入文 The assignment statement

代入文を使うことで、新しく値を変数やメモリ領域に代入できる。

assignment.nim

var x = "abc" # 新しい変数`x`を宣言し、値を代入する
x = "xyz"     # `x`に新たな値を代入する

=は代入演算子である。
現在、代入演算子をオーバーロード(拡張)・上書き・禁止することは出来ないが、今後のバージョンで変わりうる。
複数の変数をまとめて宣言する際、代入演算子を用いると、全ての変数に同じ値を代入できる。11
assign_at_once.nim

var x, y = 3  # 3を変数`x`と変数`y`に代入
echo "x ", x  # "x 3"を出力
echo "y ", y  # "y 3"を出力
x = 42        # `y`を維持しつつ、`x`を42にする。
echo "x ", x  # "x 42"を出力
echo "y ", y  # "y 3"を出力

複数の変数をまとめて宣言し代入するときに、プロシージャが呼び出されると、思わぬ動作をすることは覚えておいてほしい。12
コンパイルするときに、宣言・代入文は解きほぐされ、結果としてプロシージャを複数回呼び出すことになるからだ。
もし、返り値が副作用にプロシージャに左右される物であれば、変数に別の値が入ってしまうかもしれない。
まとめて代入する時は、この点に気を付けよう。13

定数 Constants

定数は値と強く結びついた識別子14である。
定数の値は変えられない。
コンパイラは、コンパイルの間に定数宣言中の式を計算しておく必要がある。15
 
const.nim

const x = "abc" # 定数xは文字列"abc"を含む

constの後にインデントを入れると、ひとまとまりの定数を並べて定義できる。

const_at_once.nim

const
  x = 1
  # ここにもコメントが書ける
  y = 2
  z = y + 5 # 計算が可能だ

let文 The let statement

let文はvar文のような働きを持つが、単一代入変数の宣言に用いられる。
つまり、初期化後、改めて値を代入できないのだ。

let.nim(注:コンパイルエラーを吐きます)

let x = "abc" # 新たな変数`x`を宣言し、値と結びつける
x = "xyz"     # 不正な操作: `x`への代入

let文とconst文の違いは以下のとおりである。
letとは再代入できない変数である。
constとは"コンパイル時に強制的に計算され、データ領域に代入しておく"

const_readLine.nim(注:コンパイルエラーを吐きます)

const input = readLine(stdin) # エラー:定まった値を返す式が必要

let_readLine.nim

let input = readLine(stdin)   # 実行できる

制御文 Control flow statements

最初の"greetings.nim"プログラムは、全てが順番に実行される3つの文から構成される。
これで上手く作れるのは、簡単なアルゴリズムだけである。
要するに、分岐や繰り返しも必要だ。

if文 If statement

if文はプログラムを分岐させる一つの方法である。

greeings_if.nim(注:stdinに""を入力したい場合は、一回改行をしておかないと、runtimeエラーを吐きます)

let name = readLine(stdin)
if name == "":
  echo "憐れな。名を失ったのか?"
elif name == "名前":
  echo "面白いね、君の名前は名前なのかい。"
else:
  echo "やあ、", name, "!"

if文では0個以上のelifを入れる事ができ、elseは任意である。
elifというキーワードはelse ifの略であり、過剰なインデントを回避するために使う。
(コード中の""は空の文字列を示す。文字を含まない)

case文16 Case statement

プログラムを分岐させるもう一つの方法はcase文を使う。case文は複数に分岐できる。

greetings_case.nim

let name = readLine(stdin)
case name
of "":
  echo "憐れな。名を失ったのか?"
of "name":
  echo "面白いね、君の名前は名前なのかい。"
of "デイブ", "フランク":
  echo "かっこいい名前じゃないか。"
else:
  echo "やあ、", name, "!"

見てわかる通り、ofの後に、コンマ区切りで値を並べることも可能である。17
case文は、整数型、他の順序型や文字列型を受けることが出来る。(順序型の説明は少し後にする)
整数型やほかの順序型に関しては、範囲指定も可能である。

case_num.nim

#真下のこの文は後で説明する
from strutils import parseInt

echo "数字を入力してください: "
let n = parseInt(readLine(stdin))
case n
of 0..2, 4..7: echo "入力した数字は以下の集合に属しています: {0, 1, 2, 4, 5, 6, 7}"
of 3, 8: echo "入力した数字は3か8です"

しかし実は上のコードはコンパイル出来ない。
それは、nが取りうるすべての値を網羅する必要があるのに、このコードでは0..8の範囲の値しか扱っていないからである。
「取りうる全ての整数を書く」事はあまり実用的でない(範囲指定の記法で可能ではあるが)。そのため、このコンパイルエラーを直すには、他の値に関しては特に実行することが無い事をコンパイラーに伝えるのがよい。

case_num1.nim

...
case n
of 0..2, 4..7: echo "入力した数字は以下の集合に属しています: {0, 1, 2, 4, 5, 6, 7}"
of 3, 8: echo "入力した数字は3か8です"
else: discard

単独のdiscard文8何も実行するな文である。
elseの付いたcase文がエラーを持たない事をコンパイラは知っており、このエラーは消えるのである。
全ての文字列型を網羅することは不可能である事は覚えておいてほしい。というのも、網羅できない事が「case文を文字列で分岐するときはelseが必要」な理由だからである。

一般的に、case文は部分範囲型や列挙型で使われる。これらの型の場合、取りうる値を尽くしたかのコンパイル時の確認が役に立つからである。

while文 While statement

while文は簡単なループ構造である。

ask_while.nim

echo "お名前は? "
var name = readLine(stdin)
while name == "":
  echo "名前を教えてください "
  name = readLine(stdin)
  # 新しい変数を宣言している訳ではないため、ここに``var``はない

コード例では、ユーザーが何も入力しない場合(Returnばかり押す)、入力するまで名前を聞くためにwhileループを使っている。

for文 For statement

for文はイテレータから渡される要素をループ処理するための構造である。
コード例では、組み込みのcountupイテレータを用いる。

count_for.nim(注:コンパイルエラーを吐きます。18 こちらなら動きます)

echo "10まで数える: "
for i in countup(1, 10):
  echo $i
# --> 1 2 3 4 5 6 7 8 9 10 をそれぞれ別の行に出力する。

組み込み演算子である$は整数型(int)や他のデータ型を文字列型に変換する。
変数iはforループにより暗黙に宣言されており、countupint型を返すため、iも同じ型を持つ。
iは1,2,...,10と変わっていく。
それぞれのiの値はechoで出力される。
以下のコードも同じ操作をする。

count_while.nim(注:コンパイルエラーを吐きます。18 こちらは動きます。)

echo "10まで数える: "
var i = 1
while i <= 10:
  echo $i
  inc(i) # iを1だけインクリメントする。
# --> 1 2 3 4 5 6 7 8 9 10 をそれぞれ別の行に出力する。

カウントダウンは同じくらい簡単にできる。(が、同じほどよく使われる訳ではない)

countdown.nim(注:コンパイルエラーを吐きます。18 こちらなら動きます)

echo "10から1にカウントダウンする: "
for i in countdown(10, 1):
  echo $i
# --> Outputs 10 9 8 7 6 5 4 3 2 1 on different lines

プログラムでカウントアップする事は、かなりよくある事であり、Nimは..という同じ動作をするイテレータを持つ。

count_for_withdots.nim(注:コンパイルエラーを吐きます。18 こちらなら動きます)

for i in 1..10:
  ...

スコープとblock文 Scopes and the block statement

制御文について、まだ話していないことがある。
それは「新しいスコープを作る」ということだ。
この事は要するに、以下の例ではxはループ外で取得できない、という意味だ。

block_scope.nim(注:コンパイルエラーを吐きます)

while false:
  var x = "やあ"
echo x # 実行できない

while文やfor文は暗黙にブロックを作ります。
識別子は、それが宣言されたブロックからしか見えません。
block文は明示的にブロックを作るのに使われます。

block.nim(注:コンパイルエラーを吐きます)

block myblock:
  var x = "hi"
echo x # こちらも動かない

ブロックのラベル(コード例においてはmyblock)は任意である。

break文 Break statement

break文を使うとブロックを途中で抜けることができる。
break文が抜けることができるブロックはwhileforblock文によるブロックである。
break文により、最も内側のブロックを抜ける事が出来るが、抜けるブロックをラベルで指定出来る。

break.nim

block myblock:
  echo "ブロックに入る"
  while true:
    echo "ループの中"
    break # ループからは抜ける、ブロックからは抜けない
  echo "まだループの中"

block myblock2:
  echo "ブロックに入る"
  while true:
    echo "ループの中"
    break myblock2 # ブロックを抜ける(ループからも)
  echo "まだループの中"

continue文 Continue statement

他のプログラミング言語と同様、continue文により、直ちに次の反復処理に移る事が出来る。

continue.nim (注:runtimeエラーを吐きます19

while true:
  let x = readLine(stdin)
  if x == "": continue
  echo x

when文 When statement

コード例:

when.nim20 

when system.hostOS == "windows":
  echo "Windowsで動かしていますね!"
elif system.hostOS == "linux":
  echo "Linuxで動かしていますね!"
elif system.hostOS == "macosx":
  echo "Mac OS Xで動かしていますね!"
else:
  echo "私の知らないオペレーション・システムです"

when文はif文とほとんど同じだが、いくつかの違いを持つ。
- 書かれる条件式は全て、コンパイラが計算できるように定まった値を持たなくてはならない。
- 分岐内の文は新しいスコープを持たない。
- コンパイラは、構文を解析し、コンパイル時点でtrueだった最初の分岐の中の式のみをコンパイルする。

when文は各プラットフォームに適したコードをそれぞれ書くときに便利であり、C言語における#ifdef文と似ている。

一口メモ:コメントアウトするコードが大きい場合、普通のコメントを使うよりもwhen false:を使った方が良い事がよくある。この方法なら、ネストが可能だからだ。

文とインデント Statements and indentation

基本的な制御文を押さえたところで、Nimのインデント規則の話題に戻ろう。

Nimにおいては、単純文複合文が区別される。
単純文は他の文を含むことができない。
単純文に属するのは、代入文やプロシージャ呼び出し文、return文である。
一方、ifwhenforwhileといった複合文は他の文を含むことができる。
「曖昧さ回避」のため、複合文はインデントを必要とするが、単純文はインデントする必要はない。

simple_complex_statement.nim21(注:コンパイルエラーを吐きます)22

# 代入文が一つなので、インデントは必要ない
if x: x = false

# 入れ子構造のif文にはインデントが必要である
if x:
  if y:
    y = false
  else:
    y = true

# 条件式の後に文が二つ続くため、インデントが必要である。
if x:
  x = false
  y = false

式は通常、値を返し、文の一部である。
例として、if文中の条件式が挙げられる。
式の中にインデントを入れることで、可読性を高めることも可能である。

indented_expression.nim(注:コンパイルエラーを吐きます)23

if thisIsaLongCondition() and
    thisIsAnotherLongCondition(1,
       2, 3, 4):
  x = true

経験則として、式中のインデントが良いのは演算子・閉じていない括弧・コンマの後である。

括弧とセミコロン(;)を使うことで、式しか許されない場所でも文が使える。

statement_as_expression.nim

# fac(4)、つまり4の階乗をコンパイル中に計算する
const fac4 = (var x = 1; for i in 1..4: x *= i; x)

プロシージャ Procudures

これまでのコード例で出てきた、echoreadLineのような新しい命令を定義するには、プロシージャという概念が必要となる。(メソッド関数と呼ぶ言語もある24
Nimで新しいプロシージャを定義するには、procというキーワードを使う。

proc_yes.nim

proc yes(question: string): bool =
  echo question, " (はい/いいえ)"
  while true:
    case readLine(stdin)
    of "はい", "そうです", "可", "〇": return true
    of "いいえ", "ちがいます", "不可", "×": return false
    else: echo "「はい」か「いいえ」で明確に答えてください"

if yes("全ての君の重要なファイルを消そうか?"):
  echo "デイブ、申し訳ない。私には出来ない。"
else:
  echo "君も私と同じくらい、なにが問題かわかっているようだね。"

この例では、yesという名前のプロシージャが登場する。
yesquestionに格納される質問をユーザーにした上で、「はい」やそれに似た返事に対してはtrueを、「いいえ」や似た返事に対してはfalseを返り値として返す。
return文があると、直ちにプロシージャから抜ける。(従って、whileループも抜ける)
コード例中の(question :string): boolという構文は、「questionというstring型のパラメータがあり、bool型の値を返すこと」を示す。
bool型(ブーリアン型)の話をすると、これは組み込みのデータ型であり、truefalseしか有効な値を持たない。
また、if文やwhile文などの条件式はブーリアン型でなくてはいけない。

少し用語を解説しよう。コード例中のquestionは(正式な)25パラメータと呼ばれ、"全ての君の…"の部分はパラメータに渡される引数と呼ばれる。

result変数 Result variable

返り値を返すプロシージャでは、返り値を表すresultが暗黙に宣言されている。26
式が後に続いていないreturn文はreturn resultの省略形である。
出口のreturnが存在しないプロシージャは、終わる際に必ず、自動的にresultの値を返す。
result.nim

proc sumTillNegative(x: varargs[int]): int =
  for i in x:
    if i < 0:
      return
    result = result + i

echo sumTillNegative() # 0を出力する
echo sumTillNegative(3, 4, 5) # 12を出力する
echo sumTillNegative(3, 4 , -1 , 6) # 7を出力する

result変数はプロシージャの最初の行で宣言されており、'var result'と再宣言する事は、result変数を同じ名前の普通の変数で覆い隠し、本来のresult変数が持つ機能を失う事を意味する。27
result変数はプロシージャの返す型のデフォルト値で初期化28されている。
参照型のresult変数は、プロシージャが宣言されたときはnilの値をとるため、状況に応じて、手動で初期化をしなくてはならない。

パラメータ Parameters

プロシージャの中身に記述されたパラメータは、定数として扱われる。
つまり、何も変更しないと、パラメータの値は変えられないのだ。
というのもその方が、より効率的にコンパイラによるパラメータの受け渡しが出来るからである。
プロシージャ内でミュータブルな変数を使いたければ、プロシージャ内でvar文を使って宣言しなければならない。
パラメータ名は上書き可能であり、それどころか、実は慣用的に用いられる。

shadow_parameter.nim29

proc printSeq(s: seq, nprinted: int = -1) =
  var nprinted = if nprinted == -1: s.len else: min(nprinted, s.len)
  for i in 0 .. <nprinted:
    echo s[i]

呼び出し先の引数をプロシージャで変えるには、varパラメータを使う。

var_parameter.nim30

proc divmod(a, b: int; res, remainder: var int) =
  res = a div b        # 整数型の除算
  remainder = a mod b  # 整数型のmod計算

var
  x, y: int
divmod(8, 5, x, y) # xとyを変更する
echo x
echo y

コード例の中のresremaindervarパラメータである。
varパラメータはプロシージャで変更することが可能であり、呼び出し先にも引数の変更が伝わっている。31
上の例では、varパラメータを使うよりも、タプルを返した方が適切であることは覚えておいてほしい。32

discard文 Discard statement

副作用として返り値を持つプロシージャを呼び出しつつ、その返り値を無視するには、discard文が絶対に必要である。

discard_result.nim(注:yesプロシージャも定義してあります。)

discard yes("つまらない質問をしてもよろしいですか?")

呼び出したプロシージャやイテレータを宣言した際にdiscardableプラグマが使われていた場合、discardで明示的に示さずとも、返り値を無視できる。

discardable_pragma.nim

proc p(x, y: int): int {.discardable.} =
  return x + y

p(3, 4) # 今は可能

コメントの項目で開設した通り、discard文はブロックコメントを作る時にも使える。

名前付き引数 Named arguments

プロシージャが多くのパラメータを持つ事はよくあり、その場合、パラメータの順番が分からなくなる。
これは特に、複雑なデータ型を形成するプロシージャでよく起こる。
従って、どの引数がどのパラメータに値を渡すのかを明確にするために、プロシージャに渡す引数は名前を付けることができる。

create_window.nim(注:コンパイルエラーを吐きます33

proc createWindow(x, y, width, height: int; title: string;
                  show: bool): Window =
   ...

var w = createWindow(show = true, title = "My Application",
                     x = 0, y = 0, height = 600, width = 800)

今や我々はcreateWindowプロシージャを呼び出すのに、名前付き引数を使っているのであり、引数の順番は関係ない。
なお、順番引数と名前付き引数を混ぜるのは可能だが、可読性を欠く。

mix_order_named_args.nim34

var w = createWindow(0, 0, title = "My Application",
                     height = 600, width = 800, true)

パラメータが受け取る引数の個数が、丁度1個になる事はコンパイラによって確かめられる。

デフォルト値 Default values

createWindowプロシージャを使いやすくするには、デフォルト値を持たせればいい。
呼び出し時に指定を受けない限り、このデフォルト値が引数として使われる。

default_value.nim(注:コンパイルエラーを吐きます)33

proc createWindow(x = 0, y = 0, width = 500, height = 700,
                  title = "unknown",
                  show = true): Window =
   ...

var w = createWindow(title = "My Application", height = 600, width = 800)

これでcreateWindowはデフォルト値と違う引数だけを指定すればよくなった。
デフォルト値を使って、パラメータの型推論が行われることを覚えておいてほしい。つまり例えば、titel: string = "unknown"と書く必要はない。

プロシージャのオーバーロード Overloaded procedures

Nimでは、C++のようにプロシージャが上書きできる。

overloaded_procedure.nim35

proc toString(x: int): string = ...
proc toString(x: bool): string =
  if x: result = "true"
  else: result = "false"

echo toString(13)   # toString(x: int)プロシージャを呼び出す
echo toString(true) # toString(x: bool)プロシージャを呼び出す

toStringが普通はNimの$演算子で書かれることを覚えておいてほしい)
どのプロシージャが呼び出されたtoStringとして適切かは、コンパイラが選ぶ。
このオーバーロードを解決するアルゴリズムに関しては、ここではなくマニュアルで詳しく扱う。
とはいえ、思いつきもしない嫌なアルゴリズムではなく、比較的簡単な単一化のためのアルゴリズムに基づいている。
曖昧なプロシージャ呼び出しに対しては、エラーが返される。

演算子 Operators

Nimのライブラリでは、オーバーロードが非常によく使われる。
一つの理由としては挙げられるのは、+のような演算子それぞれが、まさにオーバーロードされたプロシージャである事だ。36
中置表記a + bも前置表記も+ aパーサーで解釈できる。
中置表記の演算子は引数を必ず2つ取り、前置表記の演算子は必ず一つ取る。
後置表記があると、曖昧になるため、後置演算子は出来ない。
曖昧になるというのは例えば、a @ @ b(a) @ (@b)(a@) @ (b)のどちらを意味するのかが分からない、ということだ。
Nimでは後置表記をなくす事で、これは必ず(a) @ (@b)である事、が言えるようになったのである。

andornotなどの、ほんのいくつかの組み込みの演算子を除き、演算子は以下の記号から構成される+ - * \ / < > = @ $ ~ & % ! ? ^ . |
ユーザー定義の演算子は作れる。
@!?+~という独自の演算子を定義することも可能だが、こんな演算子を定義した場合は可読性が損なわれるかもしれない。37

演算子の計算の優先順序は、演算子の最初の文字で決まる。マニュアルに詳しい説明がある。

新しい演算子を定義するには、演算子の記号をバックティック` `で挟めばいい。38

my_dollar.nim39

proc `$` (x: myDataType): string = ...
# オーバーロードの問題は解決できるため、今後は$演算子をmyDataTypeでも使える
# $演算子を他の演算子に対して使っても、作動することは保証されている

他のプロシージャを呼び出す時のように演算子を呼び出したい時も、` `記法が使える。

call_operator.nim40 41

if `==`( `+`(3, 4), 7): echo "True"

前方宣言 Forward declarations

全ての変数やプロシージャは、使う時点で宣言されている必要がある。
(というのも、Nimと同じくらい広くメタプログラミングが使えるプログラミング言語では同程度以上の事が必要になるのである)42
しかし、相互再帰なプロシージャを使う場合、このルールに従うことが出来ない。
forward_declaration.nim43(注:以下の両コードを含みます)

# 前方宣言
proc even(n: int): bool
proc odd(n: int): bool =
  assert(n >= 0) # nが負数になり無限ループする事がない事を保証する
  if n == 0: false
  else:
    n == 1 or even(n-1)

proc even(n: int): bool =
  assert(n >= 0) # nが負数になり無限ループする事がない事を保証する
  if n == 1: false
  else:
    n == 0 or odd(n-1)

コード例では、oddevenに依存し、逆もまたしかりである。
したがって、evenを完全に定義する前であっても、コンパイラにevenの存在を知らせる必要があるのだ。
そのための構文は簡単であり、=とプロシージャの中身を省略するだけでいい。
assertは境界条件を与えているに過ぎず、モジュールの項で触れる。

今後のNimのバージョンにおいては、前方宣言に要求されるものが減らされる。

コード例から分かるもう一つの事として、プロシージャの中身は単一の式だけでも十分だという事だ。
この場合、式の値が暗黙に返り値とされる。

イテレータ以降の項は後編に含まれます

後編へ

訳注


  1. 恐らくRammsteinのMorgensternという歌からの引用。引用意図はぶっちゃけよく分からない。 

  2. 原文では"generate runtime checks"である。runtime checkが良く分かっていないため、変な訳になっているかもしれない。 

  3. 原文では"the system module which is implicitly imported"とwhichの制限用法で記述されているが、リンクに飛ぶ限り、システムモジュールは単一のモジュールであり、非制限用法的に訳している。ていうかこの著者、ちょいちょいコンマが足りない。 

  4. 原文では「バックスラッシュ」で書かれている。 

  5. 原文では"HTML code templates"とあるが、templatesの指す意味が今一つ掴めなかった。まさかフレームワーク的な意味じゃあるまいし。 

  6. 原文では"syntax tree"とのみあるが、通常のコメントと比較した文脈であると考えて、「抽象」を補った。 

  7. 原文では、"input file"とあるが、コメントの記述という文脈から、".nim"ファイルの事だと解釈した。Nimはメタプログラミングが売りであり、そういう意味の"imput file"を含んでいるのかもしれない。 

  8. 原文通りのリンクを貼っているが、こちらのほうが、適切に思える。 

  9. 原文でもyes(""])の記述がある。マニュアルを読む限り、このようなプロシージャは存在しないが、「ユーザーが定義したプロシージャyes()」をコメントアウトした、という文脈と考えて、訳した。実際、後でyes()というプロシージャが定義されている。 

  10. ideoneと手元の実行環境で確認したが、10_0000_0000という日本風の表記も可能である。西洋かぶれの3桁区切りが嫌いな方は是非。 

  11. var x, y = 1, 3という表記(つまり、同時に宣言し、別々の変数を代入)は出来ないようである。ideoneでも手元の実行環境でも同じinvalid indentationと出てきた。正直、このエラーメッセージはバグレベルで分かりにくいと思うのだが… 

  12. 原文では、修飾関係が入り組んでいる。"declaring multiple variables with a single assignment which calls a procedure"の"with a single assignment"は"declaring"に、"which calls a procedure"は"variables"にかかっていると考えているが、訳中ではそこをぼかしている。というのも、"calls a procedure"するのは、「変数」などではなく「値」であるように思えて仕方ないからである。呼ばれた回数を返すプロシージャとか。 

  13. 原文では、"For safety use only constant values."という文であるが、"constant value"が厄介なため、まるっと意訳した。流石に後述のconstで宣言される定数とは違うだろうし、「定数値」という訳が適切とも思えない。(定数と紛らわしいし) 

  14. 原文では"symbol"となっている。シンボルの定義があいまいに思えるため、「識別子」とした。訳としては離れるが、意味としては遠くないはずだ。 

  15. これはコンパイルの開発者に向けて書かれていると考える。というのも、Nimはいまだにver1.0に到達しておらず、(憶測ではあるが)コンパイル開発者もコミュニティに多くいるはずだからである。とはいえ、これが何を意味するかは明白である。例えば、.nim上では定数にプロシージャ経由で値を与えていても、.exe上ではその返り値がもう計算され、代入されているのである。さすコン。 

  16. 元となった言語はよく分からないが、よくあるswitch-caseに比べて英語の文法としては自然に見える。 

  17. breakがないのは、コンマ区切りが使えるからであろう。 

  18. ideoneのバージョンが古く(0.11.2)、何かと色々があると考えられる。インストールした環境(0.15.2)では問題なく動く。ideoneのコードでは$i()で挟んだ。パッと見た限り動作、出力に違いはない。 

  19. これは手元の実行環境では問題なく動く。というのも、このコードは無限ループで繰り返し入力を求めるコードであるからだ。stdinから有限個のinputしか受け付けないideoneがランタイムエラーを吐くのは仕方ないと言える。ちなみに、このエラーはecho xの後にbreakを挿入する事などで回避できるが、挙動は変わる。こちらでは、try-exceptを使っており、おそらく挙動としては最も近い。 

  20. どうやらideoneはlinuxを使っているようだ。(出力を見ながら) 

  21. コードの最初のコメントは原文において、"no indentation needed for single assignment statement"と書かれている。この"single"は「一つの」と訳したが、"single statement"「単純」の間に"assignment"が割り込んでいると考えられない訳ではない。これを「一つの」と訳したのは、3つ目のコメントと比較してである。(3つ目は代入文が二つある) 

  22. xもyも宣言されてないんだから、しょうがない。どうしても確認して安心したい方はこちら。 

  23. 動くわけない。エラーのないコードはこちらから 

  24. この文を読む限り、Nimにはメソッド・プロシージャ・関数の間に厳密な区別を設けようと考えてはなさそうだ。この中の日本語訳は「プロシージャ」で統一したが、別の意見があればコメントで勝手に討論してほしい。 

  25. 原文では"formal"となっているため「正式な」と訳したが、意味合いとしては「狭義の」と同じであると私は考えている。 

  26. 「暗黙に宣言されている」と言っても、実際にはproc hoge(): type =の部分が宣言に相当している。現に、typeにどのデータ型を割り当てるかによって、resultの型も変わる。また、type =を抜かすと「resultが宣言されていない」という趣旨のエラーが返ってくる。 

  27. 原文では"shadow"とのみあるが、「本来のresult変数が持つ機能を失う事を意味する」と補った。これは具体的なコード例を見る限り、"shadow"はかなり強めの意味を持つからである。 

  28. 初期値をイジれそうな方法はいくつか思いつく。私が思いついたのはブロック内でresult = initial_valueを書くproc hoge() : type = initial_valueと宣言文で初期化するパラメータ名をresultにするの3つである。どれが上手くいくかは、実際に確かめてみてほしい。また、他にうまくいった方法があれば、知らせてほしい。 

  29. ここでは関数の宣言のみをしている。実際に挙動を確かめたい人はprintSeq(@[1,2,3], 2)などと打ち込むとよいだろう。 

  30. マニュアルを読む限り、セミコロン;は単に見やすさの問題らしい。 

  31. 原文中の"visible to the caller"の訳がよく分かっていない。文脈から察するに、「プロシージャ内に止まらず、呼び出し先にも変更が影響している」という意味だと判断したが、"visible"という単語のすわりが悪い。「見えている=変化を知っている=伝わっている」と考えて訳したが、「スコープ的な意味で見える」「プロシージャ宣言文を見なくても呼び出し先を見るだけで、プログラマにはどの変数が変えられたかが見て取れる」という解釈が無理とは思えない。(別解のうちの後者は"visible from"の方がニュアンスとしては近い) 

  32. コード例はこちらから 

  33. Window型が定義されていないため、致し方ない。domというライブラリの中に、Window型がある。いろいろ調べてみたが、javascriptプラットフォームを使うくらいしか分からなかった。nim js createWindow.nimを実行するとエラーなく実行できたが非常に長いファイルで、読む気が失せたため、Gistで共有しておく。というより、私のような初心者プログラマにわかるわけない。 

  34. エラーを回避するため、createWindowプロシージャの型は整数型にしてある。 

  35. リンク先では...discardと置き換えたが、つまらないため、ローマ数字に置き換えるコードも書いてみた。あまり綺麗なコードではないので、参考にしない事。(アドバイスください)言うまでもないが、$にローマ数字に置き換える機能などない。 

  36. 原文では、"each operator like + is a just an overloaded proc"となっている。この"a just an"を私は知らないため、最初の"a"は誤植であると考え、"each operator like + is just an overloaded proc"と取り、「まさに」と訳した。ただ、「まさに」と受け取るには"just an"が微妙なので、誤訳の可能性は非常に高い。 

  37. 原文では、"@!?+~ operator, but readability can suffer."となっている。これはスペース区切りでないため、@!?+~は一つの長い演算子と考える事が出来る。そのため、続く"but readability can suffer"も、この演算子を使う事を指しており、ユーザー定義の演算子について書いているわけではないと解釈した。(これがスペース区切りであった場合、「@ ! ? + ~といったよく使われる演算子をポンポン使うと可読性が低くなるぞ」という文章と取れなくもなくなる。実際にそんな事はないと思うけど) 

  38. 私のWindowsPCの日本語キーボードではSHIFT+@で打ち込める。英語キーボードの場合、1の左にあることが多い。 

  39. コンパイルエラーを避けるために、type myDataType = objectを先頭に書き加えている。 

  40. 念のため書いておくと、\==`( `+`(3, 4), 7)`==`(3 + 4, 7)と同じであり、3 + 4 == 7`とも同じである。 

  41. 本当にどうでもいいことだが、Nimで真偽の真を意味するのはtrueではなく、Trueである。 

  42. この文章は文法が入り組んでいるので、少し解説しておく。原文は"The reason for this is that it is non-trivial to do better than that in a language that supports meta programming as extensively as Nim does."となっている。まず、全体の構造は"The reason for A is B."(Aである理由は、Bである)である。ここでAは"this"(=前の文の「宣言されている必要がある」)、Bは"it is non-trivial ... Nim does"が対応する。次にBの構造は"it is C to D"(DすることはCだ)である。Cは"non-trivial"(=些細でない(=重要・必要なこと))であり、Dは"do better ... Nim does"となる。さらに、Dの構造を考えると"do better than E in F"(=Fにおいては、Eよりも良くする)となる。ここで「良くする」というのは"better"を訳したものであるが、betterの意味はかなり広く、この文においては「より丁寧に・より十分に」くらいが当てはまるだろう。さて、Dの構造の中のE、Fに話を戻すと、Eは"that"(=前の文の「宣言されている必要がある」)であり、Fは"a language that ... Nim does"である。Fは"a language"に"that"以下の"supports ... does"が掛かる構造をしており、"supports meta programming as extensively as Nim does"は「Nimと同じくらい広く、メタプログラミングを支援している」と直訳できる。あとは、この論理をまとめれば良いが、分かりにくいため意訳している。 

  43. 原文ではコード中のコメントに"negative recursion"と書かれているが、適切な訳が見つからなかった。恐らく、nが間違って負数になり、そのまま無限ループに陥ることを心配していると考えて、訳した。