#チュートリアルの訳の前に
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の旅を始めよう。
# これはコメントである
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プロシージャが文字列型を返す事」を把握しているからである。(これはローカル型推論と呼ばれる。)
よって以下の記述も可能である。
var name = readLine(stdin)
Nimにおける型推定が基本的に、このローカル型推定である事を覚えておいてほしい。これは簡潔さと可読性とが歩み寄った結果である。
echo
・readLineといった、コンパイラが既に把握している識別子がこの"hello world"コードには含まれている。
これらの組み込まれた識別子は、システムモジュールで宣言されており、これは他の全てのモジュールが暗黙にインポートする。3
##字句要素(リテラルや識別子のこと) Lexical elements
Nimの字句要素を詳しく見てみよう。
他のプログラミング言語と同じくNimを構成する要素は、(文字列)リテラル、識別子、キーワード、コメント、演算子、その他記号である。
###文字列リテラル、文字リテラルString and character literals
文字列リテラルは二重引用符"
で囲まれる。一方の文字リテラルは単引用符'
で囲まれる。
特殊文字をエスケープするには\
を用いる。例えば、\n
は改行、\t
はタブを意味する。
raw文字列リテラルもある。4
r"C:\program files\nim"
raw文字列リテラル中の\
はエスケープ文字ではない。
文字列リテラルを記述する最後の方法は、long文字列リテラルという第三の方法である。
long文字列リテラルは3つの二重引用符に挟まれる。""" ... """
複数行にわたって文字列を記述でき、\
もエスケープ文字にならない。
使用例としては、HTMLコードを埋め込む、などがある。5
###コメント Comments
文字列リテラル・文字リテラルの中でなければ、コメントは#
で記述できる。
ドキュメント・コメントは##
が先頭に付く。
# コメント
var myVariable: int ## ドキュメントコメント
ドキュメント・コメントは特別な存在である。
ドキュメント・コメントは抽象構文木6の中に含まれるため、.nim
ファイル中7の決まった場所にしか記述できないのだ。
これにより、シンプルなドキュメンテーション・ツールで事足りる。
discard文8とlong文字列リテラルを組み合わせて使うことで、ブロックコメントを書く、という方法もある。
discard """この中ならインデントの心配をすることなく
どんなコードもコメントアウトできる。
yes("つまらない質問をしてもよろしいですか?")"""
###数値 Numbers
数値リテラルの書き方は、他の多くの言語と同じである。
また、アンダーバー_
を使って可読性を上げることもできる。(100万:1_000_000
10)
##var文 The var statement
var文を使うことで、ローカル変数やグローバル変数を新しく宣言できる。
var x, y: int # xとyが``int``型であることの宣言
var
文の後にインデントを入れると、ひとまとまりの変数を並べて宣言できる。
var
x, y: int
# ここにコメントを入れることも可能である
a, b, c: string
##代入文 The assignment statement
代入文を使うことで、新しく値を変数やメモリ領域に代入できる。
var x = "abc" # 新しい変数`x`を宣言し、値を代入する
x = "xyz" # `x`に新たな値を代入する
=
は代入演算子である。
現在、代入演算子をオーバーロード(拡張)・上書き・禁止することは出来ないが、今後のバージョンで変わりうる。
複数の変数をまとめて宣言する際、代入演算子を用いると、全ての変数に同じ値を代入できる。11
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
##定数 Constants
定数は値と強く結びついた識別子14である。
const x = "abc" # 定数xは文字列"abc"を含む
const
の後にインデントを入れると、ひとまとまりの定数を並べて定義できる。
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 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
let name = readLine(stdin)
case name
of "":
echo "憐れな。名を失ったのか?"
of "name":
echo "面白いね、君の名前は名前なのかい。"
of "デイブ", "フランク":
echo "かっこいい名前じゃないか。"
else:
echo "やあ、", name, "!"
見てわかる通り、of
の後に、コンマ区切りで値を並べることも可能である。17
case文は、整数型、他の順序型や文字列型を受けることが出来る。(順序型の説明は少し後にする)
整数型やほかの順序型に関しては、範囲指定も可能である。
#真下のこの文は後で説明する
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 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文は簡単なループ構造である。
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ループにより暗黙に宣言されており、countupがint
型を返すため、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
文が抜けることができるブロックはwhile
、for
、block
文によるブロックである。
break
文により、最も内側のブロックを抜ける事が出来るが、抜けるブロックをラベルで指定出来る。
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 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
文である。
一方、if
、when
、for
、while
といった複合文は他の文を含むことができる。
「曖昧さ回避」のため、複合文はインデントを必要とするが、単純文はインデントする必要はない。
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
経験則として、式中のインデントが良いのは演算子・閉じていない括弧・コンマの後である。
括弧とセミコロン(;)
を使うことで、式しか許されない場所でも文が使える。
# fac(4)、つまり4の階乗をコンパイル中に計算する
const fac4 = (var x = 1; for i in 1..4: x *= i; x)
##プロシージャ Procudures
これまでのコード例で出てきた、echo、readLineのような新しい命令を定義するには、プロシージャという概念が必要となる。(メソッドや関数と呼ぶ言語もある24)
Nimで新しいプロシージャを定義するには、proc
というキーワードを使う。
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
という名前のプロシージャが登場する。
yes
はquestion
に格納される質問をユーザーにした上で、「はい」やそれに似た返事に対してはtrueを、「いいえ」や似た返事に対してはfalseを返り値として返す。
return
文があると、直ちにプロシージャから抜ける。(従って、whileループも抜ける)
コード例中の(question :string): bool
という構文は、「question
というstring
型のパラメータがあり、bool
型の値を返すこと」を示す。
bool
型(ブーリアン型)の話をすると、これは組み込みのデータ型であり、true
とfalse
しか有効な値を持たない。
また、if文やwhile文などの条件式はブーリアン型でなくてはいけない。
少し用語を解説しよう。コード例中のquestion
は(正式な)25パラメータと呼ばれ、"全ての君の…"
の部分はパラメータに渡される引数と呼ばれる。
###result変数 Result variable
返り値を返すプロシージャでは、返り値を表すresult
が暗黙に宣言されている。26
式が後に続いていないreturn
文はreturn result
の省略形である。
出口のreturn
が存在しないプロシージャは、終わる際に必ず、自動的にresult
の値を返す。
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
文を使って宣言しなければならない。
パラメータ名は上書き可能であり、それどころか、実は慣用的に用いられる。
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
パラメータを使う。
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
コード例の中のres
とremainder
がvarパラメータである。
varパラメータはプロシージャで変更することが可能であり、呼び出し先にも引数の変更が伝わっている。31
上の例では、varパラメータを使うよりも、タプルを返した方が適切であることは覚えておいてほしい。32
discard_result.nim(注:yes
プロシージャも定義してあります。)
discard yes("つまらない質問をしてもよろしいですか?")
呼び出したプロシージャやイテレータを宣言した際にdiscardable
プラグマが使われていた場合、discard
で明示的に示さずとも、返り値を無視できる。
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
プロシージャを呼び出すのに、名前付き引数を使っているのであり、引数の順番は関係ない。
なお、順番引数と名前付き引数を混ぜるのは可能だが、可読性を欠く。
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++のようにプロシージャが上書きできる。
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)
である事、が言えるようになったのである。
and
、or
、not
などの、ほんのいくつかの組み込みの演算子を除き、演算子は以下の記号から構成される+ - * \ / < > = @ $ ~ & % ! ? ^ . |
。
ユーザー定義の演算子は作れる。
@!?+~
という独自の演算子を定義することも可能だが、こんな演算子を定義した場合は可読性が損なわれるかもしれない。37
演算子の計算の優先順序は、演算子の最初の文字で決まる。マニュアルに詳しい説明がある。
新しい演算子を定義するには、演算子の記号をバックティック` `
で挟めばいい。38
proc `$` (x: myDataType): string = ...
# オーバーロードの問題は解決できるため、今後は$演算子をmyDataTypeでも使える
# $演算子を他の演算子に対して使っても、作動することは保証されている
他のプロシージャを呼び出す時のように演算子を呼び出したい時も、` `
記法が使える。
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)
コード例では、odd
はeven
に依存し、逆もまたしかりである。
したがって、even
を完全に定義する前であっても、コンパイラにeven
の存在を知らせる必要があるのだ。
そのための構文は簡単であり、=
とプロシージャの中身を省略するだけでいい。
assert
は境界条件を与えているに過ぎず、モジュールの項で触れる。
今後のNimのバージョンにおいては、前方宣言に要求されるものが減らされる。
コード例から分かるもう一つの事として、プロシージャの中身は単一の式だけでも十分だという事だ。
この場合、式の値が暗黙に返り値とされる。
#イテレータ以降の項は後編に含まれます
#訳注
-
恐らくRammsteinのMorgensternという歌からの引用。引用意図はぶっちゃけよく分からない。 ↩
-
原文では"generate runtime checks"である。runtime checkが良く分かっていないため、変な訳になっているかもしれない。 ↩
-
原文では"the system module which is implicitly imported"とwhichの制限用法で記述されているが、リンクに飛ぶ限り、システムモジュールは単一のモジュールであり、非制限用法的に訳している。ていうかこの著者、ちょいちょいコンマが足りない。 ↩
-
原文では"HTML code templates"とあるが、templatesの指す意味が今一つ掴めなかった。まさかフレームワーク的な意味じゃあるまいし。 ↩
-
原文では、"input file"とあるが、コメントの記述という文脈から、".nim"ファイルの事だと解釈した。Nimはメタプログラミングが売りであり、そういう意味の"imput file"を含んでいるのかもしれない。 ↩
-
原文でも
yes(""])
の記述がある。マニュアルを読む限り、このようなプロシージャは存在しないが、「ユーザーが定義したプロシージャyes()
」をコメントアウトした、という文脈と考えて、訳した。実際、後でyes()
というプロシージャが定義されている。 ↩ -
ideoneと手元の実行環境で確認したが、
10_0000_0000
という日本風の表記も可能である。西洋かぶれの3桁区切りが嫌いな方は是非。
小数点.
やe
を含む数字は浮動小数点型になる。(10億:1.0e9
)
16進数は0x
、2進数は0b
、8進数は0o
がそれぞれ先頭に付く。先頭に0
を付けたとしても、8進数には出来ない。 ↩ -
var x, y = 1, 3
という表記(つまり、同時に宣言し、別々の変数を代入)は出来ないようである。ideoneでも手元の実行環境でも同じinvalid indentation
と出てきた。正直、このエラーメッセージはバグレベルで分かりにくいと思うのだが… ↩ -
原文では、修飾関係が入り組んでいる。"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
で宣言される定数とは違うだろうし、「定数値」という訳が適切とも思えない。(定数と紛らわしいし) ↩ -
原文では"symbol"となっている。シンボルの定義があいまいに思えるため、「識別子」とした。訳としては離れるが、意味としては遠くないはずだ。
定数の値は変えられない。
コンパイラは、コンパイルの間に定数宣言中の式を計算しておく必要がある。15 ↩ -
これはコンパイルの開発者に向けて書かれていると考える。というのも、Nimはいまだにver1.0に到達しておらず、(憶測ではあるが)コンパイル開発者もコミュニティに多くいるはずだからである。とはいえ、これが何を意味するかは明白である。例えば、.nim上では定数にプロシージャ経由で値を与えていても、.exe上ではその返り値がもう計算され、代入されているのである。さすコン。 ↩
-
元となった言語はよく分からないが、よくある
switch-case
に比べて英語の文法としては自然に見える。
プログラムを分岐させるもう一つの方法はcase文を使う。case文は複数に分岐できる。 ↩ -
break
がないのは、コンマ区切りが使えるからであろう。 ↩ -
ideoneのバージョンが古く(0.11.2)、何かと色々があると考えられる。インストールした環境(0.15.2)では問題なく動く。ideoneのコードでは
$i
を()
で挟んだ。パッと見た限り動作、出力に違いはない。 ↩ ↩2 ↩3 ↩4 -
これは手元の実行環境では問題なく動く。というのも、このコードは無限ループで繰り返し入力を求めるコードであるからだ。
stdin
から有限個のinput
しか受け付けないideoneがランタイムエラーを吐くのは仕方ないと言える。ちなみに、このエラーはecho x
の後にbreakを挿入する事などで回避できるが、挙動は変わる。こちらでは、try-except
を使っており、おそらく挙動としては最も近い。 ↩ -
どうやらideoneはlinuxを使っているようだ。(出力を見ながら) ↩
-
コードの最初のコメントは原文において、"no indentation needed for single assignment statement"と書かれている。この"single"は「一つの」と訳したが、"single statement"「単純」の間に"assignment"が割り込んでいると考えられない訳ではない。これを「一つの」と訳したのは、3つ目のコメントと比較してである。(3つ目は代入文が二つある) ↩
-
この文を読む限り、Nimにはメソッド・プロシージャ・関数の間に厳密な区別を設けようと考えてはなさそうだ。この中の日本語訳は「プロシージャ」で統一したが、別の意見があればコメントで勝手に討論してほしい。 ↩
-
原文では"formal"となっているため「正式な」と訳したが、意味合いとしては「狭義の」と同じであると私は考えている。 ↩
-
「暗黙に宣言されている」と言っても、実際には
proc hoge(): type =
の部分が宣言に相当している。現に、type
にどのデータ型を割り当てるかによって、resultの型も変わる。また、type =
を抜かすと「resultが宣言されていない」という趣旨のエラーが返ってくる。 ↩ -
原文では"shadow"とのみあるが、「本来の
result
変数が持つ機能を失う事を意味する」と補った。これは具体的なコード例を見る限り、"shadow"はかなり強めの意味を持つからである。 ↩ -
初期値をイジれそうな方法はいくつか思いつく。私が思いついたのはブロック内で
result = initial_value
を書く、proc hoge() : type = initial_value
と宣言文で初期化する、パラメータ名をresultにするの3つである。どれが上手くいくかは、実際に確かめてみてほしい。また、他にうまくいった方法があれば、知らせてほしい。 ↩ -
ここでは関数の宣言のみをしている。実際に挙動を確かめたい人は
printSeq(@[1,2,3], 2)
などと打ち込むとよいだろう。 ↩ -
原文中の"visible to the caller"の訳がよく分かっていない。文脈から察するに、「プロシージャ内に止まらず、呼び出し先にも変更が影響している」という意味だと判断したが、"visible"という単語のすわりが悪い。「見えている=変化を知っている=伝わっている」と考えて訳したが、「スコープ的な意味で見える」「プロシージャ宣言文を見なくても呼び出し先を見るだけで、プログラマにはどの変数が変えられたかが見て取れる」という解釈が無理とは思えない。(別解のうちの後者は"visible from"の方がニュアンスとしては近い) ↩
-
コード例はこちらから
###discard文 Discard statement
副作用として返り値を持つプロシージャを呼び出しつつ、その返り値を無視するには、discard
文が絶対に必要である。 ↩ -
Window
型が定義されていないため、致し方ない。domというライブラリの中に、Window
型がある。いろいろ調べてみたが、javascriptプラットフォームを使うくらいしか分からなかった。nim js createWindow.nim
を実行するとエラーなく実行できたが非常に長いファイルで、読む気が失せたため、Gistで共有しておく。というより、私のような初心者プログラマにわかるわけない。 ↩ ↩2 -
エラーを回避するため、createWindowプロシージャの型は整数型にしてある。 ↩
-
リンク先では
...
をdiscard
と置き換えたが、つまらないため、ローマ数字に置き換えるコードも書いてみた。あまり綺麗なコードではないので、参考にしない事。(アドバイスください)言うまでもないが、$
にローマ数字に置き換える機能などない。 ↩ -
原文では、"each operator like
+
is a just an overloaded proc"となっている。この"a just an"を私は知らないため、最初の"a"は誤植であると考え、"each operator like+
is just an overloaded proc"と取り、「まさに」と訳した。ただ、「まさに」と受け取るには"just an"が微妙なので、誤訳の可能性は非常に高い。 ↩ -
原文では、"
@!?+~
operator, but readability can suffer."となっている。これはスペース区切りでないため、@!?+~
は一つの長い演算子と考える事が出来る。そのため、続く"but readability can suffer"も、この演算子を使う事を指しており、ユーザー定義の演算子について書いているわけではないと解釈した。(これがスペース区切りであった場合、「@ ! ? + ~
といったよく使われる演算子をポンポン使うと可読性が低くなるぞ」という文章と取れなくもなくなる。実際にそんな事はないと思うけど) ↩ -
私のWindowsPCの日本語キーボードでは
SHIFT+@
で打ち込める。英語キーボードの場合、1の左にあることが多い。 ↩ -
コンパイルエラーを避けるために、
type myDataType = object
を先頭に書き加えている。 ↩ -
念のため書いておくと、
\
==`( `+`(3, 4), 7)は
`==`(3 + 4, 7)と同じであり、
3 + 4 == 7`とも同じである。 ↩ -
本当にどうでもいいことだが、Nimで真偽の真を意味するのはtrueではなく、Trueである。 ↩
-
この文章は文法が入り組んでいるので、少し解説しておく。原文は"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と同じくらい広く、メタプログラミングを支援している」と直訳できる。あとは、この論理をまとめれば良いが、分かりにくいため意訳している。 ↩
-
原文ではコード中のコメントに"negative recursion"と書かれているが、適切な訳が見つからなかった。恐らく、
n
が間違って負数になり、そのまま無限ループに陥ることを心配していると考えて、訳した。 ↩