Nim Tutorial (Part I)

Author: Andreas Rumpf

Introduction

"人間は目の動物ですから、美しいものを望みます。 -- schöne Dinge wünsch ich mir.""

このドキュメントは、プログラミング言語 Nim のチュートリアルです。

このチュートリアルでは、変数や型、文などの基本的なプログラミングの概念を知っていることを前提としています。 このチュートリアルでは、変数、型、ステートメントなどの基本的なプログラミングの概念に精通していることを前提としています。

Nimを学ぶための他のリソースをいくつか紹介します。

このチュートリアルのすべてのコード例は、Nimの他のドキュメントにあるものと同様に、Nim style guideに従っています。

The first program

まず、”hello world “のプログラムを修正して、ツアーを開始します。

# これはコメントです。
echo "What's your name? "
var name: string = readLine(stdin)
echo "Hi, ", name, "!"

このコードを “greetings.nim “というファイルに保存します。それでは、コンパイルして実行してみましょう。

nim compile --run greetings.nim

run switch を使うと、Nim はコンパイル後に自動的にファイルを実行します。プログラムにコマンドライン引数を与えるには、ファイル名の後に引数を追加します。

nim compile --run greetings.nim arg1 arg2

よく使うコマンドやスイッチには略語があるので、それを利用することもできます。

nim c -r greetings.nim

これはデバッグバージョンです。リリース版をコンパイルするには

nim c -d:release greetings.nim

デフォルトでは、Nim コンパイラは、デバッグの楽しみのために多数のランタイムチェックを生成します。-d:releaseでは、いくつかのチェックがオフになり、最適化がオンになります

ベンチマークやプロダクションコードには、-d:release スイッチを使用してください。C のような安全ではない言語と性能を比較する場合は、意味のある比較可能な結果を得るために、-d:danger スイッチを使用してください。そうしないと、C言語では利用できないチェックによって、Nimが不利になる可能性があります。

プログラムが何をしているかは一目瞭然だと思いますが、ここでは構文を説明します。インデントされていない文は、プログラムの開始時に実行されます。インデントとは、Nim がステートメントをグループ化する方法です。インデントはスペースのみで行われ、タビュレータは使用できません。

文字列リテラルはダブルクオートで囲まれています。var文は、readLineプロシージャが返す値を持つ、string型のnameという新しい変数を宣言します。コンパイラはreadLineが文字列を返すことを知っているので、宣言の中で型を省略することができます(これは local type inferenceと呼ばれます)。ですから、これもうまくいきます。

var name = readLine(stdin)

これは基本的に、Nimに存在する唯一の型推論の形式であることに注意してください。これは、簡潔さと読みやすさの間の良い妥協点です。

“hello world “のプログラムには、コンパイラがすでに知っているいくつかの識別子が含まれています。例えば、echoreadLineなどです。これらの組み込み関数は system モジュールで宣言されており、他のモジュールから暗黙のうちにインポートされています。

字句の要素

他のプログラミング言語と同様に、Nim は(文字列)リテラル、識別子、キーワード、コメント、演算子、その他の句読点で構成されています。

文字列リテラルと文字リテラル

文字列リテラルはダブルクォートで、文字リテラルはシングルクォートで囲みます。特殊文字は\でエスケープされます。\nは改行、\tはタブレータなどです。文字列リテラルには のものもあります。

r"C:\program files\nim"

生のリテラルでは、バックスラッシュはエスケープ文字ではありません。

文字列リテラルの3番目で最後の書き方は、long-string literalsです。文字列リテラルは、3つの引用符を使って書きます。""" …””” また、複数の行にまたがることができ、\もエスケープ文字ではありません。これは、HTMLコードのテンプレートを埋め込むときなどに非常に便利です。

コメント

コメントは、文字列または文字リテラルの外側のどこかで、ハッシュ文字 # で始まります。ドキュメントのコメントは ## で始まります。

# コメント

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

ドキュメントのコメントはトークンです。構文ツリーに属するため、入力ファイルの特定の場所でしか使用できません。この機能により、よりシンプルなドキュメント生成が可能になります。

複数行のコメントは、#[で始まり、]#で終わります。マルチラインコメントはネストすることもできます。

#[
この中にNimコードのテキストをインデントの制限なしにコメントアウトすることができます。
      yes("May I ask a pointless question?")
  #[
     Note: these can be nested!!
  ]#
]#

数値

数字のリテラルは、他の多くの言語と同じように書かれます。1_000_000(one million)のように、アンダースコアを使うこともできますが、これは読みやすくするためです。1.0e9(10億)のように、ドット(または「e」や「E」)を含む数字は浮動小数点リテラルです。16進数リテラルにはプレフィックスとして0x、2進数リテラルには0b、8進数リテラルには0oが付きます。先頭の0だけでは8進数にはなりません。

var文

var文は、新しいローカル変数またはグローバル変数を宣言します。

var x, y: int # x と y が `int` 型であることを宣言します。

varキーワードの後にインデントを使用すると、変数のセクション全体を列挙することができます。

var
  x, y: int
  # ここでもコメントが発生します
  a, b, c: string

定数

定数とは、ある値に束縛される記号です。定数の値は変更できません。コンパイラは、定数宣言の式をコンパイル時に評価できなければなりません。

const x = "abc" # 定数xは、文字列 "abc "を含みます。

インデントは、constキーワードの後に使用して、定数のセクション全体を列挙することができます。

const
  x = 1
  # ここでもコメントが発生します
  y = 2
  z = y + 5 # 演算が可能

letステートメント

let文は、var文と同じように動作しますが、宣言されたシンボルは、単式代入変数となります。初期化された後、その値は変更できません。

let x = "abc" # 新しい変数 `x` を導入し、それに値をバインドします。
x = "xyz"     # Illegal: assignment to `x`

letconstの違いは、letは再割り当てできない変数を導入すること、constは「コンパイル時の評価を強制してデータセクションに入れる」ことです。

const input = readLine(stdin) # Error: constant expression expected
let input = readLine(stdin) # works

代入文

代入文は、変数に新しい値を割り当てるもので、より一般的には記憶場所に値を割り当てます。

var x = "abc" # 新しい変数 `x` を導入し、その値を割り当てます。
x = "xyz"     # 新しい値を`x`に割り当てる

=代入演算子 です。代入演算子はオーバーロードすることができます。1つの代入文で複数の変数を宣言しても、すべての変数が同じ値になります。

var x, y = 3  # 変数 `x` と `y` に 3 を割り当てます。
echo "x ", x  # outputs "x 3"
echo "y ", y  # outputs "y 3"
x = 42        # `y`を変更せずに`x`を42に変更します。
echo "x ", x  # outputs "x 42"
echo "y ", y  # outputs "y 3"

制御フロー文

greetingsのプログラムは、3つの文を順番に実行していきます。分岐やループも必要なので、最も原始的なプログラムだけがこれで済む。

if文

if文は、制御の流れを分岐させる一つの方法です。

let name = readLine(stdin)
if name == "":
  echo "Poor soul, you lost your name?"
elif name == "name":
  echo "Very funny, your name is name."
else:
  echo "Hi, ", name, "!"

ゼロまたはそれ以上の elif 部分があります。また、elseの部分はオプションです。 キーワード elifelse if の略で、過剰なインデントを避けるのに便利です。(""は空の文字列です。 文字を含みません)。

Caseステートメント

ブランチのもう一つの方法は、ケース・ステートメントです。case statementはマルチブランチです。

let name = readLine(stdin)
case name
of "":
  echo "Poor soul, you lost your name?"
of "name":
  echo "Very funny, your name is name."
of "Dave", "Frank":
  echo "Cool name!"
else:
  echo "Hi, ", name, "!"

見ての通り、ofの枝には、カンマで区切られた値のリストも許されます。

case文では、整数、その他の序列型、文字列を扱うことができます。(整数やその他の序列型では、値の範囲を指定することもできます。

# この記述については後で説明します。
from std/strutils import parseInt
echo "A number please: "
let n = parseInt(readLine(stdin))
case n
of 0..2, 4..7: echo "The number is in the set: {0, 1, 2, 4, 5, 6, 7}"
of 3, 8: echo "The number is 3 or 8"

その理由は、n に含まれるすべての値をカバーしなければならないのに、このコードでは 0..8 という値しか扱わないからです。他のすべての可能な整数をリストアップするのはあまり実用的ではないので(範囲表記のおかげで可能ですが)、他のすべての値に対しては何もしないようにコンパイラに指示することでこれを修正します。

...
case n
of 0..2, 4..7: echo "The number is in the set: {0, 1, 2, 4, 5, 6, 7}"
of 3, 8: echo "The number is 3 or 8"
else: discard

空のdiscard文は、何もしない文です。コンパイラは、else部分を持つcase文は失敗しないことを知っているので、このエラーは消えます。文字列のケースが常に else の分岐を必要とするのはこのためです。

一般的に、case文はサブレンジ型や列挙型に使用され、可能性のあるすべての値をカバーしているかどうかをコンパイラがチェックするのに非常に役立ちます。

while文

while文は、シンプルなループ構造です。

echo "What's your name? "
var name = readLine(stdin)
while name == "":
  echo "Please tell me your name: "
  name = readLine(stdin) # ここでは新しい変数を宣言しないので、`var`はありません。

この例では、ユーザーが何も入力しない(RETURNを押すだけ)限り、ユーザーに名前を尋ね続けるwhileループを使用しています。

For文

for文は、iteratorが提供する任意の要素をループするための構文です。この例では、組み込みのcountupというイテレータを使っています。

echo "Counting to ten: "
for i in countup(1, 10):
  echo i
# --> Outputs 1 2 3 4 5 6 7 8 9 10 on different lines

変数 ifor ループによって暗黙的に宣言されており、型は int です。なぜなら、countupが返す値がint型だからです。i は 1, 2, ..., 10 の値を実行します。それぞれの値は echo` されます。このコードも同じです。

echo "Counting to 10: "
var i = 1
while i <= 10:
  echo i
  inc i # increment i by 1
# --> Outputs 1 2 3 4 5 6 7 8 9 10 on different lines

プログラムではカウントアップが頻繁に行われるので、Nimには同じことを行うイテレータもあります。

for i in 1 .. 10:
...

カウントダウンも同様に簡単に実現できます(ただし、必要とされる頻度は低い)。

echo "Counting down from 10 to 1: "
for i in countdown(10, 1):
  echo i
# --> Outputs 10 9 8 7 6 5 4 3 2 1 on different lines

ゼロベースのカウントには、2つのショートカット ...<.... ^1 (後方指数演算子)を使って、上位の指数よりも1つ小さい数までのカウントを簡単に行うことができます。

for i in 0 ..< 10:
  ...  # the same as 0 .. 9

or

var s = "some string"
for i in 0 ..< s.len:
  ...

or

var s = "some string"
for idx, c in s[0 .. ^1]:
  ... # ^1は最後の要素、^2はその1つ前の要素、というように。

コレクション(配列やシーケンスなど)のための便利なイテレータには次のようなものがあります。

  • itemsmitems は、それぞれイミュータブルとミュータブルな要素を提供します。
  • 要素とインデックス番号を提供する pairsmpairs (それぞれイミュータブルとミュータブル)
for index, item in ["a","b"].pairs:
  echo item, " at index ", index
# => a at index 0
# => b at index 1

スコープとブロックステートメント

制御フロー文には、新しいスコープを開くという、これまで説明してこなかった機能があります。つまり、次の例では、ループの外では x にアクセスできないということです。

while false:
  var x = "hi"
echo x # does not work

while(for)文は、暗黙のブロックを導入します。識別子は、宣言されたブロックの中でのみ表示されます。block文を使うと、新しいブロックを明示的に開くことができます。

block myblock:
  var x = "hi"
echo x # does not work either

ブロックのラベル(例ではmyblock)はオプションです。

break文

break文でブロックを途中で抜けることができます。break文は、whileforblockのいずれかの文を残します。ブロックのラベルが与えられていない限り、最も内側の構造体を残します。

block myblock:
  echo "entering block"
  while true:
    echo "looping"
    break # ループを離れるが、ブロックを離れません。
  echo "still in block"
echo "outside the block"

block myblock2:
  echo "entering block"
  while true:
    echo "looping"
    break myblock2 # ブロック(とループ)を離れる
  echo "still in block" # プリントされない
echo "outside the block"

Continueステートメント

他の多くのプログラミング言語と同様に、continue文は次の反復処理を直ちに開始します。

for i in 1 .. 5:
  if i <= 3: continue
  echo i # will only print 4 and 5

When文

例:

when system.hostOS == "windows":
  echo "running on Windows!"
elif system.hostOS == "linux":
  echo "running on Linux!"
elif system.hostOS == "macosx":
  echo "running on Mac OS X!"
else:
  echo "unknown operating system"

when文は、if文とほぼ同じですが、以下のような違いがあります。

  • 各条件はコンパイラによって評価されるため、定数式である必要があります。
  • 枝の中の文は新しいスコープを開きません。
  • コンパイラはセマンティクスをチェックして、trueと評価された最初の条件に属するステートメントに対してのみコードを生成します。

この when 文は、C言語の #ifdef構造と同様に、プラットフォーム固有のコードを記述するのに便利です。

ステートメントとインデント

さて、基本的な制御フローのステートメントについて説明しましたが、Nimのインデントルールに戻りましょう。

Nimでは、単純な文複雑な文を区別しています。単純な文には、他の文を含めることはできません。代入、プロシージャコール、return文などはすべて単純な文です。複雑なステートメントは、if, when, for, while のように、他のステートメントを含むことができます。曖昧さを避けるために、複雑なステートメントは常にインデントされなければなりませんが、単一の単純なステートメントはインデントされません。

# シングルアサインメントの場合、インデントは不要:
if x: x = false
# ネストしたif文に必要なインデント。
if x:
  if y:
    y = false
  else:
    y = true

# 条件の後に2つのステートメントが続くので、インデントが必要です。
if x:
  x = false
  y = false

とは、通常は値を生成するステートメントの一部です。if文の条件は式の一例です。式は、読みやすくするために、特定の場所にインデントを入れることができます。

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

原則として、式の中でのインデントは、演算子の後、開括弧の後、カンマの後に行うことができます。

括弧やセミコロン (;) を使えば、式だけが許される文を使うことができます。

# はコンパイル時にfac(4)を計算します。
const fac4 = (var x = 1; for i in 1..4: x *= i; x)

プロシージャ

例題のechoreadLineのような新しいコマンドを定義するには、procedureという概念が必要です(言語によっては、methodfunctionsと呼ぶこともあります)。(Nimでは新しいプロシージャはprocキーワードで定義します。

proc yes(question: string): bool =
  echo question, " (y/n)"
  while true:
    case readLine(stdin)
    of "y", "Y", "yes", "Yes": return true
    of "n", "N", "no", "No": return false
    else: echo "Please be clear: yes or no"

if yes("Should I delete all your important files?"):
  echo "I'm sorry Dave, I'm afraid I can't do that."
else:
  echo "I think you know what the problem is just as well as I do."

この例では、yesという名前のプロシージャを使って、ユーザに質問を投げかけ、”yes”(あるいはそれに類するもの)と答えた場合にはtrueを返し、”no”(あるいはそれに類するもの)と答えた場合にはfalseを返します。return文はすぐにプロシージャ(つまりwhileループ)を抜けます。(question: string): bool構文は、プロシージャが string 型の question という名前のパラメータを期待し、bool 型の値を返すことを説明しています。bool型は組み込みです。boolの有効な値はtruefalseだけです。if文やwhile文の条件は,bool型でなければなりません.

いくつかの用語について説明しますと、例題の中で question は (形式的な) パラメータ と呼ばれ、"Should I..." はこのパラメータに渡される 引数 と呼ばれます。

Result変数

値を返すプロシージャには、戻り値を表す暗黙の result 変数が宣言されています。式を伴わない return 文は return result の省略形です。プロシージャの終了時に return ステートメントがない場合には、result 値は常に自動的に返されます。

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

echo sumTillNegative() # echoes 0
echo sumTillNegative(3, 4, 5) # echoes 12
echo sumTillNegative(3, 4 , -1 , 6) # echoes 7

変数 result は、関数の開始時にすでに暗黙のうちに宣言されているので、たとえば ‘var result’ で再度宣言すると、同じ名前の通常の変数にシャドウイングされてしまいます。また、result変数はその型のデフォルト値ですでに初期化されています。参照データ型はプロシージャの開始時には nil になっているので、手動で初期化する必要があるかもしれないことに注意してください。

return文を持たず、特別なresult変数を使用しないプロシージャは、その最後の式の値を返します。例えば,次のようなプロシージャがあります.

proc helloWorld(): string =
  "Hello, World!"

文字列 “Hello, World!”を返します。

パラメータ

パラメータはプロシージャ本体では不変です。デフォルトでは、パラメータの値は変更できません。これは、コンパイラが最も効率的な方法でパラメータの受け渡しを実装するためです。プロシージャ内で変更可能な変数が必要な場合は、プロシージャ本体で 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        # integer division
  remainder = a mod b  # integer modulo operation

var
  x, y: int
divmod(8, 5, x, y) # modifies x and y
echo x
echo y

この例では,resremaindervarパラメータです.varパラメータはプロシージャで変更することができ,その変更は呼び出し側から見えるようになっています.上記の例では,varパラメータを使用する代わりにタプルを戻り値として使用する方が良いことに注意してください.

Discardステートメント

副作用のためだけに値を返すプロシージャを呼び出し、その戻り値を無視するには、discard文を 必ず 使用しなければなりません。Nimでは戻り値を黙って捨てることはできません。

discard yes("May I ask a pointless question?")

呼び出された proc/iterator が discardable プラグマで宣言されている場合、戻り値は暗黙のうちに無視できます。

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

p(3, 4) # now valid

名前付き引数

プロシージャには多くのパラメータがあり、パラメータの出現順序がはっきりしないことがよくあります。これは、複雑なデータ型を構築するプロシージャの場合に特に当てはまります。そこで、プロシージャの引数に名前を付けて、どの引数がどのパラメータに属するかを明確にすることができます。

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つの引数を受け取ることをチェックします。

デフォルト値

createWindow procを使いやすくするために、default valuesを提供するべきです。これは、呼び出し側が指定しない場合に引数として使用される値です。

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 の呼び出しでは、デフォルトと異なる値を設定するだけになります。

型推論はデフォルト値を持つパラメータでも機能することに注意してください。例えば、title: string = "unknown"と書く必要はありません。

オーバーロードされたプロシージャ

Nimでは、C++のように手続きをオーバーロードすることができます。

proc toString(x: int): string =
  result =
    if x < 0: "negative"
    elif x > 0: "positive"
    else: "zero"

proc toString(x: bool): string =
  result =
    if x: "yep"
    else: "nope"

assert toString(13) == "positive" # calls the toString(x: int) proc
assert toString(true) == "yep"    # calls the toString(x: bool) proc

(なお、toString は通常、Nim では $ 演算子です)。コンパイラは、toString の呼び出しに最も適した proc を選択します。このオーバーロード解決アルゴリズムが具体的にどのように機能するかは、ここでは説明しません。– 詳細はマニュアルを参照してください。曖昧な呼び出しはエラーとして報告されます。

演算子

Nim の標準ライブラリはオーバーロードを多用しています。その理由のひとつは、+ のような各演算子がオーバーロードされた proc に過ぎないからです。パーサーでは、インフィックス記法a + b)またはプレフィックス記法+ a)で演算子を使うことができます。インフィックス演算子は常に2つの引数を受け取り、プレフィックス演算子は常に1つの引数を受け取ります。(a @ @ b(a) @ (@b) という意味なのか、(a@) @ (b) という意味なのか、曖昧になってしまうので、後置演算子は使えません。Nim には後置演算子がないので、常に (a) @ (@b) を意味します)。

and, or, not などのいくつかの組み込みキーワード演算子を除いて、演算子は常にこれらの文字で構成されます。: + - * \ / < > = @ $ ~ & % ! ? ^ . |

ユーザー定義の演算子も可能です。独自の @!?+~ 演算子を定義することを妨げるものではありませんが、そうすると可読性が低下する可能性があります。

演算子の優先順位は、その最初の文字によって決まります。詳細はマニュアルに記載されています。

新しい演算子を定義するには、演算子をバックスティック”`“で囲みます。

proc `$` (x: myDataType): string = ...
# これで、$演算子はmyDataTypeでも動作するようになり、解決をオーバーロードします。
# 以前のように組み込み型でも$が動作するようにします。

また、”`“表記は、他のプロシージャと同様に、オペレータを呼び出すのにも使用できます。

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

前方宣言

すべての変数、プロシージャなどは、使用する前に宣言する必要があります。(その理由は、Nimのようにメタプログラミングを幅広くサポートしている言語では、この必要性を回避することは自明ではないからです)。ただし、相互に再帰的な手続きについてはこれを行うことができません。

# フォワード宣言。
proc even(n: int): bool
proc odd(n: int): bool =
  assert(n >= 0) # 負の再帰に陥らないようにするために
  if n == 0: false
  else:
    n == 1 or even(n-1)

proc even(n: int): bool =
  assert(n >= 0) # 負の再帰に陥らないようにするために
  if n == 1: false
  else:
    n == 0 or odd(n-1)

ここでは、oddeven に依存し、その逆もまた然りです。したがって、evenは完全に定義される前にコンパイラに導入される必要があります。このような前方宣言の構文は単純で、=とプロシージャの本体を省略するだけです。この assert は境界条件を追加するだけで、後に Modules のセクションで説明します。

後のバージョンの言語では、前方宣言の要件が緩和される予定です。

また、この例では、プロシージャのボディが1つの式で構成され、その値が暗黙的に返されることも示しています。

イテレータ

簡単な数え方の例に戻りましょう。

echo "Counting to ten: "
for i in countup(1, 10):
  echo i

このループをサポートするcountupプロックを書くことはできるでしょうか?試してみましょう。

proc countup(a, b: int): int =
  var res = a
  while res <= b:
    return res
    inc(res)

しかし、これではうまくいきません。問題は、このプロシージャは単にreturnするだけではなく、反復処理が終了した後にリターンとcontinueをしなければならないことです。この return and continueyield 文と呼びます。さて、あとは proc キーワードを iterator に置き換えるだけですが、最初のイテレータができました。

iterator countup(a, b: int): int =
  var res = a
  while res <= b:
    yield res
    inc(res)

イテレータはプロシージャとよく似ていますが、いくつかの重要な違いがあります。

  • イテレータは for ループからのみ呼び出すことができます。
  • イテレータは return ステートメントを含むことができません (プロシージャは yield ステートメントを含むことができません)。
  • イテレータには暗黙の変数 result はありません。
  • イテレータは再帰をサポートしません。
  • コンパイラはイテレータをインライン化できなければならないので、イテレータは前方宣言できません。(この制限は、コンパイラの将来のバージョンではなくなる予定です)。

しかし、クロージャ・イテレータを使って、別の制限を受けることもできます。詳細は first-class iteratorsを参照してください。イテレータは基本的に独自の名前空間を持っているので、procと同じ名前とパラメータを持つことができます。したがって、strutils modulesplit のように、イテレータの結果を蓄積してシーケンスとして返す同名のプロックでイテレータをラップするのが一般的です。

基本的な型

このセクションでは、基本的な組み込み型と、その型で利用できる操作について詳しく説明します。

ブール

Nimのブール型はboolと呼ばれ、あらかじめ定義された2つの値truefalseで構成されています。while, if, elif, when 文の条件は bool 型でなければなりません。

bool型には,演算子 not, and, or, xor, <, <=, >, >=, !=, == が定義されている。andorの演算子は,短絡的な評価を行います.例えば,以下のようになります.

while p != nil and p.name != "xyz":
  # p.nameは、p == nilの場合、評価されません。
  p = p.next

文字

文字の型charといいます。そのサイズは常に1バイトなので、ほとんどのUTF-8文字を表すことはできませんが、マルチバイトのUTF-8文字を構成するバイトの1つを表すことはできます。その理由は、効率性にあります。圧倒的に多くのユースケースでは、UTF-8が特別に設計されているため、生成されるプログラムはUTF-8を適切に処理します。文字リテラルはシングルクォートで囲まれています。

文字は ==, <, <=, >, >= 演算子で比較することができます。$ 演算子は charstring に変換します。charは整数と混ぜることはできません。charの序数を得るには、ord演算子を使用してください。整数から char への変換は chr という演算子で行います。

文字列

文字列変数は 変更可能 なので、文字列に追加することが可能で、非常に効率的です。Nim の文字列はゼロ終端で、長さを表すフィールドを持っています。文字列の長さは組み込みの len 手続きで取得できます。長さは終端のゼロを数えません。終端ゼロへのアクセスはエラーになりますが、これは Nim の文字列をコピーせずに cstring に変換できるように存在しているだけです。

文字列の代入演算子は、文字列をコピーします。文字列を連結するには & 演算子を、文字列に追加するには add 演算子を使用します。

文字列の比較は辞書順に行われます。すべての比較演算子がサポートされています。規約では、すべての文字列はUTF-8でエンコードされますが、これは強制されません。例えば,バイナリファイルから文字列を読み込む場合,文字列は単なるバイト列である。インデックス操作 s[i] は,s の i 番目の char を意味し,i 番目の unichar を意味するわけではない。

文字列変数は空の文字列 "" で初期化されます。

整数

Nimには以下のような整数型が組み込まれています。 int int8 int16 int32 int64 uint uint8 uint16 uint32 uint64.

デフォルトの整数型は int です。整数リテラルには type サフィックス を付けて、デフォルトではない整数型を指定することができます。

let
  x = 0     # x is of type `int`
  y = 0'i8  # y is of type `int8`
  z = 0'i32 # z is of type `int32`
  u = 0'u   # u is of type `uint`

ほとんどの場合,整数はメモリ上に存在するオブジェクトを数えるために使われるので,intはポインタと同じサイズになります.

一般的な演算子である + - * div mod < <= == != > >= は整数に対して定義されています。演算子 and or xor not も整数用に定義されており,ビット単位の演算を行います.左へのビットシフトは shl 演算子で、右へのビットシフトは shr 演算子で行います。ビットシフト演算子は、常にその引数を unsigned として扱います。算術ビットシフトには、通常の乗算や除算を用いることができます。

符号なしの演算はすべて折り返し、オーバーフローやアンダーフローのエラーにはなりません。

異なる種類の整数型が使われている式では、ロスレスな 自動型変換 が行われます。ただし、型変換によって情報が失われるような場合には、RangeDefect:idx: が発生します(コンパイル時にエラーが検出できない場合)。

フロート

Nimは以下の浮動小数点型を内蔵しています: float float32 float64.

デフォルトの浮動小数点型は float です。現在の実装では、float は常に 64 ビットです。

フロートリテラルには type サフィックス を付けて、デフォルト以外のフロート型を指定することができます。

var
  x = 0.0      # x is of type `float`
  y = 0.0'f32  # y is of type `float32`
  z = 0.0'f64  # z is of type `float64`

一般的な演算子 + - * / < <= == != > >= は,浮動小数点に対して定義されており,IEEE-754規格に準拠しています。

異なる種類の浮動小数点型を用いた式では,小さい方の型が大きい方の型に変換されるという自動変換が行われます。整数型は浮動小数点型に自動変換されず、またその逆もありません。これらの変換には toInt および toFloat プロシージャーを使用してください。

型の変換

数値型の変換は、型を関数として利用することで行います。

var
  x: int32 = 1.int32   # same as calling int32(1)
  y: int8  = int8('a') # 'a' == 97'i8
  z: float = 2.5       # int(2.5) rounds down to 2
  sum: int = int(x) + int(y) + int(z) # sum == 100

内部の型表現

前述したように、組み込みの $ (stringify) 演算子は、任意の基本的な型を文字列に変換し、それを echo プロシージャを使ってコンソールに出力することができます。しかし、高度な型や独自のカスタム型は、$ 演算子を定義するまで動作しません。複雑な型の現在の値を、その $ 演算子を書かずにデバッグしたいことがあります。そのような場合には、reprプロシージャを使用することができます。このプロシージャは、あらゆる型や、サイクルのある複雑なデータグラフでも動作します。次の例は、基本的な型であっても、$reprの出力には違いがあることを示しています。

var
  myBool = true
  myCharacter = 'n'
  myString = "nim"
  myInteger = 42
  myFloat = 3.14
echo myBool, ":", repr(myBool)
# --> true:true
echo myCharacter, ":", repr(myCharacter)
# --> n:'n'
echo myString, ":", repr(myString)
# --> nim:0x10fa8c050"nim"
echo myInteger, ":", repr(myInteger)
# --> 42:42
echo myFloat, ":", repr(myFloat)
# --> 3.14:3.14

高度な型

Nimでは、type文の中で新しいタイプを定義することができます。

type
  biggestInt = int64      # 利用可能な最大の整数型
  biggestFloat = float64  # 利用可能な最大のフロート型

列挙型とオブジェクト型は、type文の中でのみ定義することができます。

列挙型

列挙型の変数には、列挙型で指定された値のうち、1つだけを割り当てることができます。これらの値は、順序付けられたシンボルのセットです。各シンボルは、内部的には整数値にマッピングされます。最初のシンボルは実行時に0、2番目は1、…と表されます。例えば、以下のようになります。

type
  Direction = enum
    north, east, south, west

var x = south     # `x` は `Direction` 型で、その値は `south` です。
echo x            # prints "south"

すべての比較演算子は、列挙型で使用できます。

列挙型のシンボルは、曖昧さを避けるために修飾することができます。 Direction.south.

$演算子は任意の列挙型の値をその名前に変換することができ、ord演算子は列挙型の値をその基礎となる整数値に変換することができます。

他のプログラミング言語との互換性を高めるために、列挙型のシンボルに明示的な序数値を割り当てることができます。ただし、序数値は昇順でなければなりません。

序列型

列挙型,整数型,charおよびbool(およびそのサブレンジ)は序列型と呼ばれます.序列型にはいくつかの特別な操作があります。

操作 コメント
ord(x) x の値を表すために使用される整数値を返します。
inc(x) x を 1 つずつインクリメントします。
inc(x, n) xn だけインクリメントします;n は整数です。
dec(x) x を 1 つデクリメントします。
dec(x, n) xn だけデクリメントします。n は整数です。
succ(x) x の後継者を返します。
succ(x, n) xn 番目の後継者を返します。
pred(x) x の前任者を返します。
pred(x, n) xn 番目の前任者を返します。

inc, dec, succ, pred の操作は RangeDefectOverflowDefect を発生させて失敗することがあります。(コードが適切なランタイムチェックをオンにしてコンパイルされている場合)

サブレンジ

サブレンジ型とは、整数型や列挙型(ベース型)からの値の範囲のことです。例:

type
  MySubrange = range[0..5]

MySubrangeintのサブレンジで、0から1までの値しか保持できません。MySubrange型の変数に他の値を代入すると、コンパイル時または実行時にエラーになります。基本型からそのサブレンジ型の1つへの代入(またはその逆)は許可されています。

systemモジュールでは、重要なNatural型を range[0..high(int)] (highは最大値を返す)と定義しています。他のプログラミング言語では、自然数の代わりに符号なし整数を使うことを推奨している場合があります。これはしばしば賢明ではありません: 数字が負にならないからといって、符号なしの算術(ラップアラウンド)を望まないでしょう。Nim の Natural 型は、このよくあるプログラミングエラーを回避するのに役立ちます。

Sets

セット型は、数学的なセットの概念をモデル化したものです。セットの基底型は、一定の大きさの序列型のみとなります。 すなわち

  • int8-int16
  • uint8/byte-uint16
  • char
  • enum

またはそれに相当するものです。符号付き整数の場合、セットの基本型は 0 ... MaxSetElements-1 の範囲内にあると定義されており、現在、MaxSetElements は常に 2^16 です。

その理由は、セットが高性能なビットベクターとして実装されているからです。これより大きな型でセットを宣言しようとするとエラーになります。

var s: set[int64] # Error: set is too large

Note: Nim にはこのような制限のない hash sets (import sets でインポートする必要があります)もあります。

セットはセットコンストラクタで作ることができます。{}`は空のセットです。空のセットは任意の具象集合型と型互換性があります。コンストラクタは、要素(および要素の範囲)を含めるためにも使用できます。

type
  CharSet = set[char]
var
  x: CharSet
x = {'a'..'z', '0'..'9'} # これは、「a」から「z」までの文字と、「0」から「9」
                         # までの数字を含むセットを構築するものです。

これらの操作はセットでサポートされています。

操作 意味
A + B 2つの集合の和
A * B 2つの集合の交わり
A - B 2つの集合の差(AからBの要素を除いたもの)
A == B 集合の等式
A <= B 部分集合の関係 (A は B の部分集合であるか、B と等しい)
A < B 厳密な部分集合の関係 (AはBの適切な部分集合である)
e in A セットメンバーシップ (Aは要素eを含む)
e notin A Aは要素eを含まない。
contains(A, e) Aは要素eを含む
card(A) Aのカーディナリティ(Aに含まれる要素の数)
incl(A, elem) A = A + {elem} と同じ。
excl(A, elem) A = A - {elem} と同じ。

ビットフィールド

セットは、プロシージャの flags の型を定義するためによく使われます。これは、整数の定数を定義してそれを or しなければならないよりも、すっきりした(そして型安全な)ソリューションです。

列挙型、セット、キャストは、以下のように一緒に使うことができます。

type
  MyFlag* {.size: sizeof(cint).} = enum
    A
    B
    C
    D
  MyFlags = set[MyFlag]

proc toNum(f: MyFlags): int = cast[cint](f)
proc toFlags(v: int): MyFlags = cast[MyFlags](v)

assert toNum({}) == 0
assert toNum({A}) == 1
assert toNum({D}) == 8
assert toNum({A, C}) == 5
assert toFlags(0) == {}
assert toFlags(7) == {A, B, C}

セットはenumの値を2の累乗に変えることに注意してください。

C言語でenumやsetを使用する場合は、distinct cintを使用してください。

Cとの相互運用性については、bitsize pragmaも参照してください。

Arrays

配列は、単純な固定長のコンテナです。配列の各要素は、同じ型を持っています。配列のインデックスの型は任意の序数型になります。

配列は [] を用いて構築することができる。

type
  IntArray = array[0..5, int] # an array that is indexed with 0..5
var
  x: IntArray
x = [1, 2, 3, 4, 5, 6]
for i in low(x) .. high(x):
  echo x[i]

x[i]という表記は,xのi番目の要素にアクセスするために使われます.配列のアクセスは常に(コンパイル時または実行時に)境界チェックが行われます。これらのチェックは、プラグマやコンパイラのコマンドラインスイッチ--bound_checks:offで無効にすることができます。

配列は、他のNimの型と同様に、値の型です。代入演算子は、配列の内容全体をコピーします。

組み込みの len プロックは、配列の長さを返します。low(a)は配列aの有効な最低のインデックスを返し、high(a)は有効な最高のインデックスを返します。

type
  Direction = enum
    north, east, south, west
  BlinkLights = enum
    off, on, slowBlink, mediumBlink, fastBlink
  LevelSetting = array[north..west, BlinkLights]
var
  level: LevelSetting
level[north] = on
level[south] = slowBlink
level[east] = fastBlink
echo level        # --> [on, fastBlink, slowBlink, off]
echo low(level)   # --> north
echo len(level)   # --> 4
echo high(level)  # --> west

他の言語での入れ子配列(多次元)の構文は、通常、各次元が他の次元と同じインデックスタイプに制限されているため、より多くのブラケットを追加する問題となります。Nimでは、異なるインデックスタイプの異なる次元を持つことができるので、入れ子の構文は若干異なります。先ほどの例では、レベルを別の列挙型でインデックスされた列挙型の配列として定義しましたが、次の行を追加することで、整数のインデックスでアクセスする高次に分割されたLightTower型を追加することができます。

type
LightTower = array[1..10, LevelSetting]
var
tower: LightTower
tower[1][north] = slowBlink
tower[1][east] = mediumBlink
echo len(tower)     # --> 10
echo len(tower[1])  # --> 4
echo tower          # --> [[slowBlink, mediumBlink, ...more output..
# 次の行は、タイプミスマッチエラーのためにコンパイルされません。
#tower[north][east] = on
#tower[0][1] = on

組み込みの len プロックが配列の一次元目の長さのみを返すことに注意してください。LightTower を定義する別の方法では、入れ子の性質をよりよく説明するために、先ほどの LevelSetting 型の定義を省略して、代わりに 1 次元目の型として直接埋め込んで記述します。

type
  LightTower = array[1..10, array[north..west, BlinkLights]]

配列がゼロから始まることはよくあることなので、ゼロから指定したインデックスから1を引いた範囲を指定するショートカットの構文があります。

type
  IntArray = array[0..5, int] # an array that is indexed with 0..5
  QuickArray = array[6, int]  # an array that is indexed with 0..5
var
  x: IntArray
  y: QuickArray
x = [1, 2, 3, 4, 5, 6]
y = x
for i in low(x) .. high(x):
  echo x[i], y[i]

シーケンス

シーケンスは、配列に似ていますが、実行中に変更される可能性のある動的な長さを持ちます(文字列のように)。シーケンスはサイズ変更が可能なので、常にヒープ上に割り当てられ、ガベージコレクションされます。

シーケンスにも、lenlowhighといった操作が可能である。x[i]という表記は,xのi番目の要素にアクセスするために用いることができる.

シーケンスは,配列コンストラクタ [] と配列からシーケンスへの演算子 @ を併用することで構築できます.シーケンスの領域を確保するもう一つの方法は,組み込みの newSeq プロシージャを呼び出すことです.

シーケンスは,オープンアレイのパラメータに渡すことができます.

例:

var
  x: seq[int] # 整数列への参照
x = @[1, 2, 3, 4, 5, 6] # @は,配列を,ヒープ上に確保されたシーケンスに変えます。

シーケンス変数は、@[]で初期化されます。

シーケンスと一緒に使う場合、for文は1つの変数でも2つの変数でも使用できます。1変数の形で使用すると、変数にはシーケンスが提供する値が格納されます。このfor文は、systemモジュールのitems()イテレータの結果をループしています。しかし、2変数形式の場合は、最初の変数がインデックスの位置を保持し、2番目の変数が値を保持することになります。ここでは、for文が、systemモジュールのpairs()イテレータの結果をループしています。例を挙げます。

for value in @[3, 4, 5]:
  echo value
# --> 3
# --> 4
# --> 5

for i, value in @[3, 4, 5]:
  echo "index: ", $i, ", value:", $value
# --> index: 0, value:3
# --> index: 1, value:4
# --> index: 2, value:5

Open arrays

注意: Openarrayはパラメータにのみ使用可能です。

固定サイズの配列は柔軟性に欠けることがよくあります。 手続きは、異なるサイズの配列を扱うことができるようにする必要があります。 openarray 型はこれを可能にします。Openarray は常に 0 から始まる int でインデックスされます. len, low, high 操作はオープン配列でも使用可能です。openarrayパラメータには、互換性のある基本型を持つ任意の配列を渡すことができ、インデックスの型は関係ありません。

var
  fruits:   seq[string]       # '@[]' で初期化された文字列の並びへの参照。
  capitals: array[3, string]  # 固定長の文字列の配列

capitals = ["New York", "London", "Berlin"]   # 配列 'capitals' は3つの要素のみを代入することができます。
fruits.add("Banana")          # シーケンス 'fruits' は実行中に動的に拡張可能です。
fruits.add("Mango")

proc openArraySize(oa: openArray[string]): int =
  oa.len

assert openArraySize(fruits) == 2     # 手続きは,パラメータとしてシーケンスを受け取ります.
assert openArraySize(capitals) == 3   # のみならず、配列型も

openarray 型はネストできません。多次元 openarray は、めったに必要とされず、効率的に実行できないため、サポートされません。

Varargs

varargs パラメータは openarray パラメータと同じようなものである。しかし、これはプロシージャに可変数の引数を渡すことを実装する手段でもある。コンパイラは引数のリストを自動的に配列に変換する。

proc myWriteln(f: File, a: varargs[string]) =
  for s in items(a):
    write(f, s)
  write(f, "\n")

myWriteln(stdout, "abc", "def", "xyz")
# is transformed by the compiler to:
myWriteln(stdout, ["abc", "def", "xyz"])

この変換は、varargsパラメータがプロシージャヘッダの最後のパラメータである場合にのみ行われます。また、このコンテキストで型変換を行うことも可能です。

proc myWriteln(f: File, a: varargs[string, `$`]) =
  for s in items(a):
    write(f, s)
  write(f, "\n")

myWriteln(stdout, 123, "abc", 4.0)
# is transformed by the compiler to:
myWriteln(stdout, [$123, $"abc", $4.0])

この例では、パラメータ a に渡されるすべての引数に対して $ が適用されます。文字列に適用される $はnopであることに注意してください。

Slices

スライスは、構文的にはサブレンジ型と似ていますが、異なる文脈で使用されます。スライスは、 ab という 2 つの境界を持つ Slice 型のオブジェクトです。スライス自体はあまり便利なものではありませんが、他のコレクションタイプでは範囲を定義するためにスライスオブジェクトを受け入れる演算子を定義しています。

var
  a = "Nim is a programming language"
  b = "Slices are useless."

echo a[7 .. 12] # --> 'a prog'
b[11 .. ^2] = "useful"
echo b # --> 'Slices are useful.'

前の例では、スライスは文字列の一部を変更するために使用されています。スライスの境界には、その型がサポートする任意の値を保持できますが、どのような値が受け入れられるかを定義するのは、スライスオブジェクトを使用する proc です。

文字列、配列、シーケンスなどのインデックスを指定する様々な方法を理解するために、Nim はゼロベースのインデックスを使用することを覚えておく必要があります。

つまり、文字列 b は長さ 19 であり、インデックスを指定する2つの方法は

"Slices are useless."
 |          |     |
 0         11    17   using indices
^19        ^8    ^2   using ^ syntax

ここで b[0 ... ^1]b[0 ... b.len-1]b[0 ..< b.len] に相当し、^1b.len-1 を指定するショートハンドの手段であることが分かります。backwards index operator を参照してください。

上記の例では、文字列の末尾がピリオドなので、「 "useless”」という部分を取得して「\”useful"」に置き換えるには、次のようにします。

b[11 ... ^2] は \”useless" の部分、b[11 ... ^2] = "useful" は \”useless" の部分を \”useful" に置き換え、結果は "Slices are useful." となります。

注1: 別の書き方として b[^8 .. ^2] = "useful" または b[11 ... b.len-2] = "useful" または b[11 ..< b.len-1] = "useful" とします。

注2: ^ テンプレートは BackwardsIndex 型の distinct int を返すので、 const lastIndex = ^1 として定義した lastIndex 定数を持ち、後で b[0 ... lastIndex] として使用することができます。

オブジェクト

異なる値を一つの構造体にまとめて名前を付けるデフォルトの型がオブジェクト型である。オブジェクトは値型であり、オブジェクトが新しい変数に代入されると、その構成要素もすべてコピーされることを意味する。

各オブジェクト型 Foo はコンストラクタ Foo(field: value, ...) を持っており、すべてのフィールドを初期化することができます。未指定のフィールドにはデフォルト値が入ります。

type
Person = object
name: string
age: int
var person1 = Person(name: "Peter", age: 30)

echo person1.name # "Peter"
echo person1.age  # 30

var person2 = person1 # copy of person 1

person2.age += 14

echo person1.age # 30
echo person2.age # 44


# 順番が入れ替わることがあります
let person3 = Person(age: 12, name: "Quentin")

# すべてのメンバーを指定する必要はありません
let person4 = Person(age: 3)
# 未指定のメンバはデフォルト値で初期化されます。この場合、それは空文字列である。
doAssert person4.name == ""

定義モジュールの外から見えるべきオブジェクトフィールドは * でマークしなければならない。

type
  Person* = object # 他のモジュールから見える型
    name*: string  # このタイプのフィールドは、他のモジュールから見える。
    age*: int

タプル

タプルは、これまで見てきたオブジェクトと非常によく似ています。代入演算子で各構成要素をコピーする値型です。しかし、オブジェクト型とは異なり、タプル型は構造的に型付けされます。つまり、同じ型で同じ名前のフィールドを同じ順序で指定すれば、異なるタプル型は等価になります。

コンストラクタ () はタプルを構築するために使用することができる。コンストラクタのフィールドの順番は、タプルの定義にある順番と一致しなければなりません。しかし、オブジェクトとは異なり、ここではタプルの型の名前を使うことはできません。

オブジェクトの型と同様に、タプルのフィールドにアクセスするには t.field という記法が使われます。オブジェクトにはないもう一つの表記法は t[i] で、i‘th フィールドにアクセスする。ここで i は定数整数でなければならない。

type
  # 型は、Personを表す。
  # Personは名前(name)と年齢(age)で構成されています。
  Person = tuple
    name: string
    age: int
  
  # 同等の型の代替構文。
  PersonX = tuple[name: string, age: int]
  
  # 匿名フィールド構文
  PersonY = (string, int)

var
  person: Person
  personX: PersonX
  personY: PersonY

person = (name: "Peter", age: 30)
# PersonとPersonXは等価である
personX = person

# 匿名フィールドを持つタプルを作成する。
personY = ("Peter", 30)

# 匿名フィールドを持つタプルは、フィールド名を持つタプルと互換性がある。
person = personY
personY = person

# 通常、短いタプルの初期化構文に使用されます。
person = ("Peter", 30)

echo person.name # "Peter"
echo person.age  # 30

echo person[0] # "Peter"
echo person[1] # 30

# タプルを別の型セクションで宣言する必要はありません。
var building: tuple[street: string, number: int]
building = ("Rue del Percebe", 13)
echo building.street

# 次の行はコンパイルできません、これらは異なるタプルです
#person = building
# --> Error: type mismatch: got (tuple[street: string, number: int])
#     but expected 'Person'

タプルの型を宣言しなくても使用できますが、異なるフィールド名で作成されたタプルは、同じフィールド型を持っていても異なるオブジェクトとして扱われます。

タプルは変数に代入する際に、unpackedすることができます。これは、タプルのフィールドを個別に命名された変数に直接代入するのに便利である。この例として、osモジュールsplitFileプロックがあり、パスのディレクトリ、名前、拡張子が同時に返されます。タプルアンパッキングが機能するためには、アンパッキングを割り当てたい値を括弧で囲む必要があります。そうしないと、個々の変数にすべて同じ値を割り当ててしまうことになります! たとえば

import std/os

let
  path = "usr/local/nimc.html"
  (dir, name, ext) = splitFile(path)
  baddir, badname, badext = splitFile(path)
echo dir      # outputs "usr/local"
echo name     # outputs "nimc"
echo ext      # outputs ".html"
# 以下はすべて同じ行を出力します。
# "(dir: usr/local, name: nimc, ext: .html)"
echo baddir
echo badname
echo badext

タプルアンパッキングは、for-loopでもサポートされています。

let a = [(10, 'a'), (20, 'b'), (30, 'c')]

for (x, c) in a:
  echo x
# This will output: 10; 20; 30

# インデックスへのアクセスも可能です。
for i, (x, c) in a:
  echo i, c
# This will output: 0a; 1b; 2c

タプルのフィールドは常にパブリックであり、オブジェクト型のフィールドなどとは異なり、エクスポートされるように明示的にマークする必要はない。

参照型とポインタ型

参照(他のプログラミング言語におけるポインタと同様)は、多対一の関係を導入する方法である。つまり、異なる参照がメモリ上の同じ場所を指し示し、変更することができます。

Nim は traceduntracedの参照を区別しています。トレースされない参照は ポインタ とも呼ばれます。トレースされた参照はガベージコレクションされたヒープ内のオブジェクトを指し、トレースされない参照は手動で割り当てられたオブジェクトやメモリ内の他の場所にあるオブジェクトを指します。したがって、トレースされない参照は安全ではありません。しかし、ある種の低レベルの操作(例えば、ハードウェアへのアクセス)には、トレースされていない参照が必要である。

トレースされた参照は ref キーワードで宣言され、トレースされない参照は ptr キーワードで宣言されます。

空の [] 添え字記法は、参照を de-refer するために使うことができ、参照が指す項目を取り出すことを意味します。. (タプル/オブジェクトフィールドへのアクセス演算子) および []` (配列/文字列/シーケンスインデックス演算子) 演算子は、参照型に対する暗黙のデリフェレンス操作を行います。

type
  Node = ref object
    le, ri: Node
    data: int

var n = Node(data: 9)
echo n.data
# n[].dataを書く必要はありません。むしろ、n[].dataは非常に推奨されません。

新しいトレースオブジェクトを割り当てるには、組み込みの手続き new を使用することができます。

var n: Node
new(n)

トレースされていないメモリを処理するために、手続き allocdeallocrealloc を使用することができます。system モジュールのドキュメントに、より詳細な情報が記載されています。

参照が nothing を指している場合、その値は nil です。

手続き型

手続き型とは、手続きへの(やや抽象的な)ポインタのことです。手続き型の変数に許される値は nil です。Nim は手続き型を使って functional プログラミング手法を実現します.

Example:

proc greet(name: string): string =
  "Hello, " & name & "!"

proc bye(name: string): string =
  "Goodbye, " & name & "."

proc communicate(greeting: proc (x: string): string, name: string) =
  echo greeting(name)

communicate(greet, "John")
communicate(bye, "Mary")

手続き型に関する微妙な問題は,手続きの呼び出し規則が型の互換性に影響する ことです.手続き型は,同じ呼び出し規則を持つ場合にのみ互換性があります.手続き型は、同じ呼び出し規約でなければ互換性がありません。異なる呼び出し規約は、 マニュアルにリストアップされています。

識別型(Distinct type)

Distinct typeは、ベースタイプとの間にサブタイプ関係を示唆しない新しいタイプを作成することができます。識別された型に対して、すべての動作を明示的に定義する必要があります。これを支援するために、distinct typeとそのベースタイプの両方は、あるタイプから他のタイプにキャストすることができます。その例は、マニュアルに記載されています。

モジュール

Nim は モジュール という概念でプログラムを分割することができます。各モジュールはそれぞれ独立したファイルになっています。モジュールによって 情報隠蔽分割コンパイルが可能になります。モジュールは importステートメントを使って、他のモジュールのシンボルにアクセスすることができます。アスタリスク(*)が付いたトップレベルのシンボルのみがエクスポートされます。

# Module A
var
x*, y: int
proc `*` *(a, b: seq[int]): seq[int] =
  # 新しいシーケンスを割り当てる。
  newSeq(result, len(a))
  # 2 つの int のシーケンスを乗算します。
  for i in 0 ..< len(a): result[i] = a[i] * b[i]

when isMainModule:
  # 新しい `*` 演算子をシーケンスに対してテストします。
  assert(@[1, 2, 3] * @[1, 2, 3] == @[1, 4, 9])

上のモジュールは x* をエクスポートしますが、 y はエクスポートしません。

モジュールのトップレベルの文はプログラムの開始時に実行されます。これは、例えば複雑なデータ構造を初期化するために使用することができます。

各モジュールは特別なマジック定数 isMainModule を持ち、そのモジュールがメインファイルとしてコンパイルされた場合に true となります。これは、上の例で示したように、モジュール内にテストを埋め込むのに非常に便利です。

モジュールのシンボルは module.symbol という構文で qualified することができます。また、シンボルがあいまいな場合は、修飾しなければなりません。シンボルは2つ(またはそれ以上)の異なるモジュールで定義され、両方のモジュールが3番目のモジュールにインポートされている場合、あいまいな状態になります。

# Module A
var x*: string
# Module B
var x*: int
# Module C
import A, B
write(stdout, x) # error: xはあいまい
write(stdout, A.x) # okay: 使用する修飾子
var x = 4
write(stdout, x) # 曖昧でない:モジュールCのxを使用する

しかし、このルールは、プロシージャやイテレータには適用されません。ここでは、オーバーロードのルールが適用されます。

# Module A
proc x*(a: int): string = $a
# Module B
proc x*(a: string): string = $a
# Module C
import A, B
write(stdout, x(3))   # no error: A.x is called
write(stdout, x(""))  # no error: B.x is called
proc x*(a: int): string = discard
write(stdout, x(3))   # ambiguous: which `x` is to call?

シンボルの除外

通常の import 文は、エクスポートされたすべてのシンボルを取り込みます。これらは、 except という修飾子を用いて、除外するシンボルを指定することで制限することができます。

import mymodule except y

From ステートメント

エクスポートされたすべてのシンボルをインポートするシンプルな import ステートメントをすでに見てきました。リストアップされたシンボルのみをインポートする別の方法として、 from import ステートメントがあります。

from mymodule import x, y, z

また、from文はシンボルの名前空間修飾を強制することができます。これにより、シンボルを使用できるようになりますが、使用するためには修飾が必要です。

from mymodule import x, y, z
x()           # use x without any qualification
from mymodule import nil
mymodule.x()  # モジュール名を接頭辞として x を修飾する必要があります。

x()           # ここで x を修飾なしで使用すると、コンパイルエラーになります。

モジュール名は一般に説明のために長くなるので、シンボルを修飾するときに使用する短い別名を定義することもできます。

from mymodule as m import nil
m.x()         # m は mymodule のエイリアスです。

include文

include ステートメントは、モジュールをインポートするのとは根本的に異なることを行います:それは単にファイルの内容をインクルードするだけです。単にファイルの内容を取り込むだけです。 include ステートメントは大きなモジュールをいくつかのファイルに分割するのに便利です。

include fileA, fileB, fileC

Part 2

さて、基本的なことがわかったところで、手続き型プログラミングのための優れた構文以外に、Nimが何を提供してくれるのか見てみましょう。パートII