tutorial
Nim
NimDay 19

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

だいぶさぼっている間にnimのバージョンが0.17.2になっていました。途中にバージョンの壁があるかもしれません。古いバージョンの訳があったらコメントに書いておいてください。

前編はこちら

イテレータ Iterators

1から10までを単に数え上げるだけの、退屈なコード例に話を戻そう。

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

echo "10まで数える: "
for i in countup(1, 10):
  echo $i

このループで使われているcountupプロシージャは自分で書けるだろうか?試してみよう。

countup_iterator.nim2

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

しかしながら、このコードは上手くいかない。
このプロシージャの問題点は、値を返すだけ(return)だという点だ。
私たちは、returnの後、次のループへcontinueするものを求めているのだ。
このreturncontinueを合わせたものこそがyield文である。
コードをyield文で修正したら、あとは、procキーワードをiteratorに書き換えればいい。
初めてのイテレータの出来上がりだ。

countup1.nim

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

イテレータはプロシージャと非常に似ているが、重要な違いが数点ある。
- イテレータはforループ処理でしか呼び出せない
- イテレータ中にreturn文を記述できず、プロシージャ中にyield文が記述できない
- イテレータではresult変数が暗黙に宣言されない
- イテレータは再帰ができない
- イテレータは前方宣言できない。というのも、コンパイラによってインライン展開出来るようにイテレータは書かれている必要があるからである。(今後のバージョンのコンパイラでは、この制限はなくなる)

clojureイテレータには、これらとは別の制限がつく。詳細は第一級イテレータを見てほしい。
同じ名前のプロシージャとイテレータでも本質的には別の名前空間を持つことから、とあるプロシージャと同じ名前、パラメータを持ったイテレータを作ることが出来る。
このため、イテレータの返す値を同じ名前のプロシージャがシーケンスとしてまとめて返すことは頻繁に見られる。
例として、strutilsモジュールsplitが挙げられるだろう。

基本的なデータ型 Basic types

このセクションでは、基本的な組み込みデータ型やそこで使える演算子について詳しく取り扱う。

ブーリアン型 Booleans

Nimではブーリアン型をboolと呼び、truefalseという前もって定義された二値の一方しかとりえない。
while, if, elif when文の条件分岐はブーリアン型を返さなくてはならない。

ブーリアン型で使える演算子として、notandorxor<<=>>=!===が定義されている。
andor演算子では真偽の評価を端折ることがある。例えば

while_and.nim(注: コンパイルエラーを起こします3

while p != nil and p.name != "xyz":
  # p == nil の場合、p.nameの真偽は評価されない 
  p = p.next

である。

文字型 Characters

Nimでは、文字型をchar型と呼び、大きさは1バイトである。
したがって、ほとんどのUTF-8文字を表現出来ない、とはいえ、マルチバイトUTF-8文字の一バイト分は表現できる。
効率を優先したのが理由だ。
しかし、UTF-8の使用頻度は圧倒的に多く、作ったプログラムはUTF-8を問題なく扱える。
これはUTF-8が、この用途のために使われたからである。4
char型リテラルは単引用符で囲まれる。

文字型は==<<=>>=演算子で比較できる。
$演算子はchar型をstring型に変換する。
文字型は整数型を混ぜて使うことは出来ず、char型から文字番号を得るにはordプロシージャを使う。
整数をASCIIで対応するchar型に変換するにはchrプロシージャを使う。5

文字列型 Strings

文字列型の値はミュータブルであり、かなり効率的に文字列を追加できる。
Nimの文字列はNull終端であり、フィールド長を持つ。
文字列のフィールド長は組み込みのlenプロシージャで得られる。
ただしこの際、終端のNull文字は頭数に入れられない。
終端のNull文字を呼び出しても、エラーではなく、より簡単なコードに繋がることがよくある。

nullTerminal.nim(※コンパイルエラーを回避するために数か所加筆してあります)

if s[i] == 'a' and s[i+1] == 'b':
  # ``i < len(s)``であるか否かを調べなくても良いのだ!
  ...

文字列に代入演算子を使うと、文字列をコピーできる。
文字列を連結するには&演算子を、追加するにはaddを用いる。

文字列の比較では辞書的な順序が使われる。
すべての比較演算子が使える。

すべての文字列は慣習的にUTF-8の文字列であるが、強制ではない。
例えば、バイナリファイルから読み取られた文字列は単にバイトを並べた物に過ぎない。
この場合、s[i]というインデックス操作はsのi番目のcharを返すのであり、sのi番目のunicharを返すわけではない。

文字列のnilという特別な値を初期値に持つ。
しかし、ほとんどの文字列演算子はnilを扱えない。(エラーに繋がる)
そのため、空の文字列にはnilより""を使うべきである。
しかし、""を使うとヒープ領域に文字列オブジェクトが作られるため、かねあいを考える必要がある。6

整数型 Integers

Nimは以下の整数型が組み込まれている
int int8 int16 int32 int64 uint uint8 uint16 uint32 uint64

デフォルトの整数型はintである。
整数型リテラルは型指定の接尾辞を用いてほかの整数型に変更できる。

intVariable.nim

let
  x = 0     # xの型は``int``である
  y = 0'i8  # yの型は``int8``である
  z = 0'i64 # zの型は``int64``である
  u = 0'u   # uの型は``uint``である

整数型が最もよく使われるのは、メモリ中のオブジェクトを数える為であり、intはポインタと同じ大きさを持つ。

整数型では、よく使われる演算子である+ - * div mod < <= == != > >=が定義されている。
また、整数型ではand or xor not演算子も定義されており、ビット演算子が使えるようになっている。
左シフト演算子はshlであり、右シフト演算子はshrである。
ビットシフト演算子は被演算子7符号なし整数として扱う。
算術シフトに関しては掛け算や割り算を使えばよい。

符号がない整数を含む演算ではラップアラウンドが起き、オーバーフロー、アンダーフローが起きない。

異なる種類の整数型が使われる式では自動型変換が行われる。
しかし型変換で情報が失われる場合8、EOutOfRange例外がraiseされる。(コンパイル時にエラーが見つからない場合)

浮動小数点型 Floats

Nimは以下の浮動小数点型が組み込まれているfloat float32 float64

デフォルトの浮動小数点型は
float
である。
現在実装されているものでは、floatは常に64bitの大きさである。

浮動小数点型リテラルは型指定の接尾辞を用いてほかの浮動小数点型に変更できる。

floats.nim

var
  x = 0.0      # xの型は``float``である
  y = 0.0'f32  # yの型は``float32``である
  z = 0.0'f64  # zの型は``float64``である

浮動小数点型では、よく使われる演算子である+ - * / < <= == != > >=が定義され、IEEE規格に沿う。

異なる種類の浮動小数点型が使われる式では自動型変換が行われる。より小さい型が大きい型に変換されるのである。
整数型が自動的に浮動小数点型に変換されることはない。逆も同様だ。
toInttoFloatプロシージャがこれらの型変換には使える。

型変換 Type conversion

基本的な型同士の変換は、型を関数として使うことで行える。

type_conversion.nim

var
  x: int32 = 1.int32   #int32(1)を呼び出すのと同様
  y: int8  = int8('a') # 'a' == 97'i8
  z: float = 2.5       # int(2.5)で2に丸められる
  sum: int = int(x) + int(y) + int(z) # sum == 100

データ型の内部表現 Internal type representation

先に述べたように、組み込みの$(文字列化)演算子はどんな基本的な型でも、文字列に変換する。これによりechoプロシージャで出力できる。
しかし、より高度な型や自分自身で定義した型は$演算子を使っても、自分自身で定義するまでは上手くいかない。
$演算子を書かずに、その複雑な型の値をデバックしたいだけ、という時がある。
そんな時はreprが使える。これは閉路を含むような参照構造を持ったデータでも使える9
以下の例では、基本的な型の間ででも$reprの出力が異なることを示している。

internal.nim10

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.1400000000000001e+00:3.1400000000000001e+00

高度なデータ型 Advanced types

biggetst.nim

type
  biggestInt = int64      # 使える最大の整数型
  biggestFloat = float64  # 使える最大のfloat型

列挙型とオブジェクト型を定義するにはtype文が必要である。11

列挙型 Enumerations

列挙型の変数には有限集合の値しか代入することができない。
この集合とは順番に並んだ識別子である。
各識別子には、内部的に整数型が代入されている。
実行時では、最初の識別子に0、二番目に1、以下同様、といった風に代入されている。
例:

enums.nim

type
  Direction = enum
    north, east, south, west

var x = south      # `x`は`Direction`型。値は`south`
echo $x           # "south"を`stdout`に書く

列挙型なら全て、比較演算子が使える。
曖昧さをなくすため、Direction.southという列挙型の識別子が使える。

$演算子はどんな列挙型でもその名前に変換し、ordプロシージャは内部的に使われている整数値を返す。

ほかの言語とよりよく親和するために12、enum型の識別子に明示的に順序型の値13を代入することができる。
ただし、その順序型の値は昇順でなくてはならない。
順序型の値が明示的に与えられていない識別子は、ひとつ前の値+1の順序型値を代入される。

明示的に定義された列挙型は穴開きでもよい。

(holy_enum.nim)[https://ideone.com/iBXiDm]

type
  MyEnum = enum
    a = 2, b = 4, c = 89

順序型 Ordinal types

跳んだ値のない列挙型、整数型、charbool(と部分範囲型)は順序型と呼ばれている。順序型にはかなり多くの特別な演算が出来る。

操作
ord(x) xの値を表すのに使われている整数を返す
inc(x) xを1だけインクリメントする
inc(x, n), xをnだけインクリメント。ただしnは整数
dec(x) xを1だけデクリメントする
dec(x, n), xをnだけデクリメントする。ただしnは整数
succ(x) xの後に来る値を返す
succ(x, n) xのn番目後に来る値を返す。ただしnは整数
pred(x) xの前に来た値を返す
pred(x, n) xのn番目前に来た値を返す。ただしnは整数

このincpredそしてpred操作はEOutOfRangeEOverflowという例外を吐いて失敗することがある。

部分範囲型 Subranges

部分範囲型はある範囲内の値を整数型や列挙型(元にする型)から取り出したものである。
例:

subrange.nim

type
  MySubrange = range[0..5]

MySubrangeは0から5までの値しか持てないintの部分範囲型である。
MySubrangeで使える値以外を代入すると、コンパイル時エラーや実行時エラーになる。
ある型から、それを元に作った部分範囲型への代入(とその逆)が可能である。

systemモジュールでは、重要な型である自然数型range[0..high(int)](highは最大値を返す)として定義されている。
ほかのプログラミング言語では、符号なし整数型を自然数として使うことを勧めているかもしれない。
しかしこれは多くの場合賢くない。というのも、負の数を取らない、というだけの理由で、(ラップアラウンドしてしまう)符号なし型の計算方法を使うのは好ましくないからだ。
NimのNatural型は、このようなよくあるプログラミングのエラーの回避に役立つ。

集合型 Sets

集合型は数学的な集合の概念をモデルにしている。集合型が元にする型は特定の大きさを持つ順序型のみである。すなわち:

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

やこれと同等のものである。
これは集合型が高パフォーマンスなビットベクトルとして実行されることが理由である。
もっと大きな型で集合型を宣言しようと試しても、エラーに終わる。

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

var s: set[int64] #Error: set is too large (訳 エラー: 集合が大きすぎます)

集合を作るには集合コンストラクタを使う、{}が空集合だ。
空集合は要素を持つ集合型とも適合する。13
コンストラクタは要素(や要素の範囲)を挟むことでも使える。

letters.nim (注:ideoneのコードには出力の行を加えています)

type
  CharSet = set[char]
var
  x: CharSet
x = {'a'..'z', '0'..'9'}  # これは'a'から'z'までの文字と、'0'から'9'までの数字を含む集合を作った

集合では以下の演算が使える。

演算 意味
A + B 二つの集合の和集合
A * B 二つの集合の積集合(共通部分)
A - B 二つの集合の差集合(AからBの要素を除いたもの)
A == B 集合の同等性
A <= B 包含関係(Aは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}と同じ

集合型がよく使われるのは、プロシージャのフラグの型を定義する時である。
この方法は、整数の定数を作りorでまとめるよりも、美しい。(型安全でもある)

配列型 Arrays

配列は単純な固定長のコンテナである。
配列内の要素はすべて同じ型である。
配列のインデックスはどんな順序型でもよい。14

配列は[]で作れる。

(arrays.nim)[https://ideone.com/tqcWzK]

type
  IntArray = array[0..5, int] # 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というコマンドラインスイッチで呼び出す。15

配列はほかのNimの型のように値型である。
代入演算子は配列の中身を丸ごとコピーする。

組み込みのlenプロシージャは配列の長さを返す。low(a)は最小の有効なインデックスを返し、highは最大のインデックスを返す。

nested_array.nim16

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 repr(level)  # --> [on, fastBlink, slowBlink, off]
echo low(level)   # --> north
echo len(level)   # --> 4
echo high(level)  # --> west

ほかの言語におけるネストされた配列(多次元)の構文では、かっこを増やすことになる。というのも通常、各次元のインデックスの型はほかの次元の型と同じ型に縛られているからだ。
Nimにおいては、異なる次元では異なる型のインデックスを持てる。そのため、インデックスのネストは少々異なる。
levelがenumの配列として定義され、enumがまた別のenumによりインデックスを振られていた直前の例を元にして、以下の数行を加えることで、整数型のインデックスからアクセスできるlight tower 型を加えることが出来る。

more_nested_array.nim17

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 repr(tower)    # --> [[slowBlink, mediumBlink, ...more output..
# 以下の行は型の不一致のエラーによりコンパイルできない
#tower[north][east] = on
#tower[0][1] = on

組み込みlenプロシージャが配列の最初の次元の長さしか返さない様子に注目してほしい。
ネストされている様子をよりよく可視化できるLightTowerを定義するもう一つの方法は、以前のLevelSetting型の定義を除き、その代わりに最初の次元の型として埋め込まれているものとして書くことである。

embed.nim

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

配列が0から始まることはよくあるため、簡略化された構文で0から(指定された値-1)までの範囲を指定できる。

shorthand_range

type
  IntArray = array[0..5, int] # 0..5の要素を持つ配列
  QuickArray = array[6, int]  # 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]

シーケンス型 Sequences

シーケンス型は配列の可変長なものに似ており、その長さは(文字列のように)実行時に変えられる。
シーケンス型が可変長なため、ヒープ領域に割り当てられ、ガーベジコレクションの対象である。

シーケンス型は常に、0から始まるintでインデックスされる。lenlowhigh
による演算はシーケンス型でも使える。
x[i]という記法はxのi番目の要素にアクセスするために使える。

シーケンスを作るには、配列のコンストラクタの[]に配列からシーケンス型を変換する演算子である@を組み合わせる。
シーケンス型に領域を割り当てるもう一つの方法は組み込みのnewSeqプロシージャを使うことである。

シーケンスはオープン配列パラメータに渡すことが出来る。
例:

array_to_seq.nim

var
  x: seq[int] # 整数のシーケンスへの参照
x = @[1, 2, 3, 4, 5, 6] # @により配列がヒープに割り当てられたシーケンスへと変換される

シーケンス型の変数はnilで初期化される。
しかし、ほとんどのシーケンス型の演算子はパフォーマンスの事情でnilに対処できない。(例外エラーを引き起こすことにつながる)
したがって、値には@[]のほうがnilよりも使うべきである。
しかし、@[]はヒープ領域にシーケンスのオブジェクトを作るため、ここではトレードオフの関係がある。

シーケンスを使うときfor文は1つないし2つの変数を持つことが出来る。
一変数形式を用いる場合、その変数はシーケンス型から渡される値を保持する。
for文はsystemモジュールのitems()からの返り値に基づいてループする。
しかし、二変数形式を使った場合、最初の変数はインデックス位置を保持し、二つ目の変数は値を保持する。
この場合、for文はsystemモジュールのpairs()からの返り値に基づいてループする。

for_types.nim

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

注意:オープン配列型はパラメータでのみ使われる

固定長の配列があまりに融通が利かないことはよくある。プロシージャが様々な長さの配列に対応しなくてはならなくなる。
オープン配列型はこの対応を可能にする。
オープン配列は常に、0から始まるint型でインデックスされる。lenlowhigh演算子はオープン配列でも使える。
どんな配列も型が対応していれば、オープン配列パラメータに渡すことができ、インデックスの型は関係ない。

open_array.nim

var
  fruits:   seq[string]       # 'nil'で初期化されている文字列への参照
  capitals: array[3, string]  # 文字列の固定長配列

fruits = @[]                  # 'fruits'として参照される空っぽのシーケンスをヒープ領域に作る

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   # のみならず配列型も受け入れる

オープン配列パラメータはネストできない。多次元オープン配列パラメータがないのは、めったに必要にならず、効率的に実装できないからである。

可変引数 Varargs

varargsパラメータはオープン配列パラメータに似ている。
しかし、多様な数の引数をプロシージャに渡すことを実装する手段でもある。
コンパイラーは引数の並びを自動的に配列へと変換する。

varargs

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

myWriteln(stdout, "abc", "def", "xyz")
# はコンパイラにより変換されて、
myWriteln(stdout, ["abc", "def", "xyz"])

この変換は可変引数パラメータがプロシージャのヘッダ18の最後のパラメータの時のみ行われる。
以下の状況では、型変換も行える。

varargs_str

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

myWriteln(stdout, 123, "abc", 4.0)
# はコンパイラにより変換されて、
myWriteln(stdout, [$123, $"abc", $4.0])

この例では、パラメータaに渡されたどの変数にも$が適用されている。
文字列に適用された$は何もしていないことに注意してほしい。

スライス Slices

スライスは構文上、部分範囲型によく似ているが、別の状況で使われる。
スライスは単に、スライス型のオブジェクトであり、二つの境界値、abを持つ。
スライスは単独ではあまり役に立たない。しかし他のコレクションにおいて、範囲を指定するためにスライスが使える。

slice.nim

var
  a = "Nim is a progamming language" #訳:Nimはプログラミング言語である
  b = "Slices are useless." #訳:スライスは役立たず

echo a[7..12] # --> 'a prog'  #訳:はプログ
b[11..^2] = "useful"  #訳:役立つ
echo b # --> 'Slices are useful.' #スライスは役立つ

直前の例において、スライスは文字列の一部を編集することに使われた。
スライスはその型で使えるどんな値でも扱えるが、どんな型が使えるかはスライスを使うプロシージャが決める。

文字列、配列、シーケンスなどのインデックスを指定する様々な方法を知るには、Nimが0始まりのインデックスを使うことを覚えておかなくてはならない。

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

"Slices are useless."
 |          |     |
 0         11    17   インデックスを使用
^19        ^8    ^2   ^構文を使用

ここではb[0..^1]b[0..b.len-1]b[0..<b.len]に等しく、^1b.len-1を指定する簡単な方法を提供していることがわかる。

上の例は文字列がピリオドで終わっているため、"useless"を除いて"useful"にするには"useless"部分がb[11..^2]であるため、b[11..^2] = "useful"が"useless"部分を"useful"に置き換え、"Slices are useful."を返す。

注意:この代替法はb[^8..^2] = "useful"だったり、b[11..b.len-2] = "useful"だったり、b[11..<b.len-1] = "useful"だったり……

タプル型 Tuples

タプル型は様々な名前のフィールドや領¥フィールド域の順序を定義する。
タプルを作るうえで、コンストラクタである()が使える。
両機の順序はタプルの定義の順序と一致しなくてはいけない。
異なったタプル型は、フィールドを同じ型で同じ名前で同じ順序で指定しているとき、同等となる。

tuples.nim

type
  Person = tuple[name: string, age: int] # 人物を表現するタプル
                                         #人物は名前と年齢により構成されている
var
  person: Person
person = (name: "Peter", age: 30)
# 同じだがより可読性が低いもの
person = ("Peter", 30)

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

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

# 異なるtypeのセクションでタプルを宣言する必要はない
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'

# 以下のコードは、フィールドの名前と型が同じなため、動く
var teacher: tuple[name: string, age: int] = ("Mark", 42)
person = teacher

タプルを使うために型を宣言する必要はないが、別のフィールド名で作られたタプルはフィールドの型が同じであっても別のオブジェクトとして認識される。

タプルは変数の代入の時に展開される。(そしてこの時だけ!)
これはタプルのフィールドを、それぞれの名前を持つ変数に代入するうえで便利だ。
この例として、osモジュール内のsplitFileプロシージャがある。これはディレクトリ名、ファイル名、ファイルの拡張子にを同時に返す。
タプルの展開をするには、展開された値を代入したい変数をかっこで囲む必要がある。そうしないと、すべての変数に同じ値を代入することになる!
例えば、

spitfire.nim

import os

let
  path = "usr/local/nimc.html"
  (dir, name, ext) = splitFile(path)
  baddir, badname, badext = splitFile(path)
echo dir      # 出力は `usr/local`
echo name     # 出力は `nimc`
echo ext      # 出力は `.html`
# 以下はすべて同じ文章を出力する
# `(dir: usr/local, name: nimc, ext: .html)`
echo baddir
echo badname
echo badext

レファレンス型とポインタ型 Reference and pointer types

レファレンス(ほかのプログラミング言語のポインタ型に似ている)は多対一の関係を導入する方法である。
これはつまり、様々な参照がメモリ上の同じところを指し、同じ所を編集できることを示す。

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

トレースされた参照はrefというキーワードで宣言される。
トレースされない参照はptrというキーワードで宣言される。

添え字が空っぽの[]はレファレンス型を逆参照することに使える。逆参照とはレファレンス型が指すものを取得することを意味する。

.(タプルやオブジェクトフィールドにアクセスする演算子)と[](配列、文字列、シーケンスのインデックスの演算子)は参照型に対して、暗示的な逆参照の演算子として働く。

reference.nim

type
  Node = ref NodeObj
  NodeObj = object
    le, ri: Node
    data: int
var
  n: Node
new(n)
n.data = 9
# n[].dataと書く必要はない。じつはn[].dataというのは強く非推奨である

トレースされるオブジェクトを新たに作るには、組み込みプロシージャのnewを使う必要がある。
トレースされないメモリを扱うには、allocdeallocreallocが使える。
systemモジュールのドキュメントに詳細が書いてある。

レファレンス型が何も指していない場合、nilの値を持つ。

プロシージャ型 Procedural type

プロシージャ型はプロシージャへの(多少、抽象的な)ポインタである。nilはプロシージャ型の変数に使える値である。
Nimはプロシージャ型を使うことで、関数型プログラミングのテクニックを使おうとしている。

procesure_type.nim

proc echoItem(x: int) = echo x

proc forEach(action: proc (x: int)) =
  const
    data = [2, 3, 5, 7, 11]
  for d in items(data):
    action(d)

forEach(echoItem)

プロシージャ型に関するちょっとした問題があり、それは呼出規約が型の適合性に影響することだ。プロシージャ型は同じ呼出規約を持たないと適合しない。
様々な呼出規約はマニュアル
に書かれている。

ディスティンクト型 Distinct type

ディスティンクト型を使って"元の型のサブタイプである関係を暗示しない"型を新しく作ることができる。
ディスティンクト型の振る舞いはすべて明示的に定義しなくてはならない。
この作業の楽にするため、ディスティンクト型と元とした型は、一方からもう一方へ引用することが出来る。
例はマニュアルに書いてある。

モジュール Modules

Nimではプログラムをモジュールの概念に基づいて分割することが出来る。
各モジュールはそれぞれのファイルを持つ。
モジュールは情報隠蔽と分割コンパイルを可能にする。
モジュールはimport文を用いることで別のモジュールの識別子を使うことが出来る。

アスタリスク(*)のしるしがつけられたトップレベルの識別子のみがほかのモジュールへエクスポートされる。

export.nim

# モジュールA
var
  x*, y: int

proc `*` *(a, b: seq[int]): seq[int] =
  # 新しいシーケンスを割り当てる
  newSeq(result, len(a))
  # 二つのシーケンスを掛け合わせる
  for i in 0..len(a)-1: result[i] = a[i] * b[i]

when isMainModule:
  # 新しく作ったシーケンスのための``*``演算子を試す
  assert(@[1, 2, 3] * @[1, 2, 3] == @[1, 4, 9])

以上のプログラムではx*がエクスポートされるが、yはされない。

各モジュールは特別なマジック定数のisMainModuleを持っている。これは、そのモジュール自体がコンパイルされるときはtrueである。上の例のように、テストを埋め込むのに便利である。

お互いに依存関係にあるモジュールを作るのは可能だが、強く非推奨である。これは一方のモジュールを、もう一方のモジュールなしで再利用できないからである。

モジュールをコンパイルするアルゴリズムは

  • モジュール全体を通常通りコンパイルし、import文を再帰的にたどる
  • もし循環があれば、すでに解析された識別子(エクスポートされた識別子)のみをインポートする。もし未知の識別子があれば、中止する

これは以下の例でよくわかる

(ideoneでの複数ファイルの使い方がわからないのでコードは略)

# モジュール A
type
  T1* = int  # モジュール A が``T1``型をエクスポートする
import B     # コンパイラがBを解析し始める

proc main() =
  var i = p(3) # Bがすでに完全に読み込まれているので動く

main()
# モジュール B
import A  # A はここでは解析されない。すでに解析されたAの識別子のみがインポートされる。

proc p*(x: A.T1): A.T1 =
  # T1がすでにAとのシンボルテーブルに追加されているため、動く
  result = x + 1

モジュールの識別子はmodule.symbol構文で修飾できる。
もし識別子が多義的であれば、修飾しなくてはならない。
ある識別子が多義的である、とは二つ以上のモジュールで定義されており、第三のモジュールにインポートされていることを指す。

# モジュールA
var x*: string
# モジュール B
var x*: int
# モジュール C
import A, B
write(stdout, x) # エラー:xが多義的 
write(stdout, A.x) # 問題なし: 修飾された

var x = 4
write(stdout, x) # 多義的でない: Cのxを用いる

しかし、このプロシージャやイテレータではこのルールが適用されない。
この場合、オーバーロードのルールが使われる。

# モジュールA
proc x*(a: int): string = $a
# モジュールB
proc x*(a: string): string = $a
# モジュールC
import A, B
write(stdout, x(3))   # エラーなし: A.x が呼び出された
write(stdout, x(""))  # エラーなし: B.x が呼び出された

proc x*(a: int): string = nil
write(stdout, x(3))   # 多義的: どっちの`x`が呼び出された?

除外修飾子 Excluding symbols

通常のimport文はすべてのエクスポートされた識別子をインポートする。
除外したい識別子をexceptで修飾して除外することで、インポートを制限できる。

import mymodule except y

from文 From statement

もう既に、エクスポートされたすべての識別子をインポートするような単純なimportは学んだ。
代替法として、列挙した識別子のみをインポートするのがfrom import文だ。

from mymodule import x, y, z

from文は識別子の修飾を強制することが出来る。これにより識別子を使えるが修飾が必要となる。

from mymodule import x, y, z

x()           # xを修飾なしで使う
from mymodule import nil

mymodule.x()  # xにはモジュール名を前置した修飾が必要

x()           # ここxを修飾なしで使うのはコンパイルエラーを起こす

モジュール名は一般に、書くには長すぎるため、修飾の時のためにより短い別名を定義できる。

from mymodule as m import nil

m.x()         # mはmymoduleの別名

include文 Include statement

include文はモジュールのインポートとは根本的に異なることをする。ただ単にファイルの中身を含める。
include文は大きすぎるモジュールを複数ファイルの分けるのに使える。

include fileA, fileB, fileC

パート2 Part 2

ようやく基礎が終わり、次はNimが手続き型言語のための素敵な構文以外に何を提供しているか、見てみよう。Part II

訳注


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

  2. このリンク先ではコンパイルエラーを起こしている。しかし、これはあまり本質的なところでエラーを返しているわけではない。というのも、「イテレータを使うべきところでプロシージャを返している」というのは、コンパイルエラーの類ではなく、論理エラーであるからだ。こちらではプロシージャ名を少しいじったうえで、本質的なエラーを返してもらっている。 

  3. pが定義されていないというエラーが出てくる。そこでvar p : stringを加えてみると、p.nameが定義されていないと出てくる。端折るのは飽くまでもランタイム時だけで、コンパイル時に無視してくれる訳ではない。(そりゃそうか)問題なく動くのはこちら 

  4. 原文を取り違えている可能性がある。特に、"the resulting programs will still handle UTF-8 properly"が未だに再現できていない。また、"as UTF-8 was specially designed for this"のthisの意味も取りかねている。というのも、UTF-8の思想が「8bit単位で処理する」で有ると同時に、それにより、「一部の文字において処理速度を上げる」と複雑だからであり、たぶん、その他諸々もあるからである。どれ? 

  5. ASCIIで対応する、という事は実際にコードを走らせることで確認したうえで補った。 

  6. 原文では、"often creates a string object on the heap"とあるが、"often"を訳していない。場合によって作ったり、作らなかったりするの? 

  7. 原文中では、"argument"(引数)となっているが、演算子にとって正確な語は"operand"(被演算子)であると考えた。ただ、Nimにおいては演算子はプロシージャと同じように宣言したら作れる事を考えると、原文中の"argument"が不適切とも言えない。 

  8. おそらく、「7.1(浮動小数点数)を7(整数型)にする」のように、「0.1という情報が失われる」という状況を想定しているのだろうが、どういう型変換をしたら、そうなるのか分からない… 

  9. 原文では"complex data graphs with cycles"となっている。調べたがどんな意味だろうか…"data graph"と言っている以上、グラフ理論のほうを指しているとは思えないのだが… コメント参照。訳は@243f6a8885a308d313198a2e037 さんのものを丸パクリしました。 

  10. コメントで示している出力、ideoneの出力、nim ver0.17.2の出力はそれぞれ異なった。(ideoneはtrue:true、n:'n'、nim:0x2b2c01b7a050"nim"、42:42、3.14:3.14。ver0.17.2はideoneはtrue:true、n:'n'、nim:000000000016F058"nim"、42:42、3.14:3.14。 

  11. 原文では、"cannot be defined on the fly, but only within a type statement."と非常にややこしいことになっているが、"not ~ but"と解釈し、また"on the fly"(直訳は「急いで」)をvartypeといったものを置かずに、と解釈した。 

  12. 親和という言葉を使っているが、本来はinterfaceという言葉である。よりよく接するため、というニュアンスから意訳した。 

  13. "concrete set type"を「要素を持つ集合型」と訳した。ちなみに「元」と「要素」とで悩んだ。 

  14. x['A']とかできる 

  15. "pragma or invoking the compiler .."となっていたが、プラグマとコマンドラインスイッチは同じなので、「つまり」と訳した。 

  16. "invalid data!"っていう見るからにやばそうな出力がある。0.17.2ではコメント通りの出力。 

  17. "invalid data!"っていう見るからにやばそうな出力がある。0.17.2ではコメント通りの出力。 

  18. 関数ヘッダのほうが一般的な表現かもしれない