Nim Tutorial (Part II)

Author: Andreas Rumpf

はじめに

“繰り返しは、ばかばかしいことを合理的にする” – Norman Wildberger

このドキュメントは、プログラミング言語 Nim の高度な構成についてのチュートリアルです。マニュアルには、より高度な言語機能の例が多数掲載されていますので、この文書は多少古くなっています。

プラグマ

プラグマとは、大量の新しいキーワードを導入することなく、コンパイラに追加の情報やコマンドを与えるための Nim の方法です。プラグマは {..} という特殊な中括弧で囲まれます。このチュートリアルでは、プラグマは扱いません。利用可能なプラグマについては manual または user guide を参照してください。

オブジェクト指向プログラミング

Nim のオブジェクト指向プログラミング(OOP)に対するサポートは最小限ですが、強力な OOP テクニックを使用することが可能です。OOP はプログラムを設計するための 一つの 方法であり、唯一の 方法ではありません。多くの場合、手続き的なアプローチは、よりシンプルで効率的なコードにつながります。特に、継承よりも合成を優先させた方が良い設計になることが多いです。

継承

Nimでの継承は完全に任意です。実行時の型情報を持つ継承を行うには、オブジェクトは RootObj から継承する必要があります。これは直接的に行うこともできるし、 RootObj を継承するオブジェクトを継承することで間接的に行うこともできる。通常、継承された型は ref 型としてマークされますが、これは厳密には強制されません。オブジェクトが特定の型であるかどうかを実行時に確認するために、 of 演算子を使用することができます。

type
  Person = ref object of RootObj
    name*: string  # は `name` が他のモジュールからアクセス可能であることを意味します。
    age: int       # *がないのは、他のモジュールからフィールドが隠されることを意味します。
  
  Student = ref object of Person # Student は Person を継承しています。
    id: int                      # idフィールドを持つ

var
  student: Student
  person: Person
assert(student of Student) # is true
# オブジェクトの構築。
student = Student(name: "Anton", age: 5, id: 2)
echo student[]

継承は object of 構文で行われます。多重継承は現在のところサポートされていない。オブジェクトタイプに適切な祖先がいない場合、 RootObj を祖先として使用することができるが、これは単なる慣習に過ぎない。先祖を持たないオブジェクトは暗黙のうちに final となります。system.RootObj とは別に、新しいオブジェクトルートを導入するために、 inheritable プラグマを使用することができます。(これは例えば GTK ラッパーで使用されています)。

Refオブジェクトは、継承が行われるときには常に使用されるべきです。これは厳密には必要ではありませんが、非Refオブジェクトでは let person.Object のような代入を行うことができます。Person = Student(id: 123) のような代入はサブクラスのフィールドを切り詰めます。

: 単純なコードの再利用のためには、継承(is-a relation)よりも合成(has-a relation)の方が望ましい場合があります。Nim ではオブジェクトは値型なので、合成は継承と同じように効率的です。

相互再帰的な型

オブジェクト、タプル、および参照は、互いに依存し合う非常に複雑なデータ構造をモデル化することができます; これらは 相互に再帰的 です。Nim ではこれらの型は1つの型セクションの中でしか宣言できません。(それ以外の場合、任意のシンボルルルックアヘッドが必要となり、コンパイルが遅くなります)。

type
  Node = ref object  # 以下のフィールドを持つオブジェクトへの参照。
    le, ri: Node     # leftとrightのサブツリー
    sym: ref Sym     # 葉には、Symの参照が含まれています。
  
  Sym = object       # シンボル
    name: string     # シンボル名
    line: int        # シンボルが宣言された行
    code: Node       # シンボルの抽象構文木

型変換

Nim は型キャストと型変換を区別しています。キャストは cast 演算子を用いて行われ、コンパイラにビットパターンを別の型に解釈するように強制します。

型変換はある型を別の型に変換するための、より丁寧な方法である。型変換は抽象的な を保持しますが、必ずしも ビットパターン を保持するわけではありません。型変換ができない場合、コンパイラは文句を言うか、例外を発生させる。

型変換のシンタックスは destination_type(expression_to_convert) です(普通の呼び出しと同じです)。

proc getID(x: Person): int =
  Student(x).id

xStudent でない場合、 InvalidObjectConversionDefect 例外が発生します。

オブジェクトのバリアント

単純なバリアントタイプが必要な状況では、オブジェクト階層が過剰になることがよくあります。

その例です。

# これは、Nimで抽象的な構文木をどのようにモデル化できるかの例です。
type
  NodeKind = enum  # 異なるノードタイプ
    nkInt,          # 整数値を持つ葉
    nkFloat,        # 浮動小数点の葉
    nkString,       # 文字列の値を持つ葉
    nkAdd,          # an addition
    nkSub,          # a subtraction
    nkIf            # an if statement
  Node = ref object
    case kind: NodeKind  # `kind` フィールドが判別子となる
    of nkInt: intVal: int
    of nkFloat: floatVal: float
    of nkString: strVal: string
    of nkAdd, nkSub:
      leftOp, rightOp: Node
    of nkIf:
      condition, thenPart, elsePart: Node

var n = Node(kind: nkFloat, floatVal: 1.0)
# n.kind の値が適合しないため、次のステートメントで `FieldDefect` 例外を発生させます。
n.strVal = ""

この例からわかるように、オブジェクト階層の利点は、異なるオブジェクトタイプ間の変換が不要であることです。しかし、無効なオブジェクトフィールドにアクセスすると、例外が発生します。

メソッド呼び出しの構文

ルーチンを呼び出すための構文シュガーがあります。methodName(obj, args)の代わりに obj.methodName(args) という構文が使えます。もし残りの引数がない場合には、括弧を省略することができます: obj.len (instead of len(obj)).

このメソッドコールの構文はオブジェクトに限定されるものではなく、どのような型にも使用することができます。

import std/strutils

echo "abc".len # は echo len("abc") と同じです。
echo "abc".toUpperAscii()
echo({'a', 'b', 'c'}.card)
stdout.writeLine("Hallo") # writeLine(stdout, "Hallo")と同じです。

(メソッド呼び出し構文の別の見方として、欠落していたpostfix記法を提供するものである)。

だから、「純粋なオブジェクト指向」のコードは簡単に書けるのです。

import std/[strutils, sequtils]

stdout.writeLine("Give a list of numbers (separated by spaces): ")
stdout.write(stdin.readLine.splitWhitespace.map(parseInt).max.`$`)
stdout.writeLine(" is the maximum!")

プロパティ

上記の例からわかるように、Nim には get-properties は必要ありません。メソッド呼び出し構文で呼び出される通常の get-procedureは同じことを実現します。しかし、値の設定は別です。このためには特別なセッター構文が必要です。

type
  Socket* = ref object of RootObj
    h: int # スターがないため、モジュール外部からはアクセスできません。

proc `host=`*(s: var Socket, value: int) {.inline.} =
  ## ホストアドレスのsetter
  s.h = value

proc host*(s: Socket): int {.inline.} =
  ## ホストアドレスのgetter
  s.h

var s: Socket
new s
s.host = 34  # `host=`(s, 34) と同じです。

(この例では インライン プロシージャも示しています)。

配列のアクセス演算子 [] はオーバーロードして、 配列のプロパティ を提供することができる。

type
  Vector* = object
    x, y, z: float

proc `[]=`* (v: var Vector, i: int, value: float) =
  # setter
  case i
  of 0: v.x = value
  of 1: v.y = value
  of 2: v.z = value
  else: assert(false)

proc `[]`* (v: Vector, i: int): float =
  # getter
  case i
  of 0: result = v.x
  of 1: result = v.y
  of 2: result = v.z
  else: assert(false)

ベクトルはタプルによってよりよくモデル化され、すでに v[] アクセスを提供しているので、この例は馬鹿げています。

動的なディスパッチ

プロシージャは常に静的ディスパッチを使用します。動的ディスパッチでは、 proc キーワードを method に置き換えてください。

type
  Expression = ref object of RootObj ## 式の抽象的な基底クラス
  Literal = ref object of Expression
    x: int
  PlusExpr = ref object of Expression
    a, b: Expression

# 気をつけよう evalは動的バインディングに依存する
method eval(e: Expression): int {.base.} =
  # この基本メソッドをオーバーライドする
  quit "to override!"

method eval(e: Literal): int = e.x
method eval(e: PlusExpr): int = eval(e.a) + eval(e.b)

proc newLit(x: int): Literal = Literal(x: x)
proc newPlus(a, b: Expression): PlusExpr = PlusExpr(a: a, b: b)

echo eval(newPlus(newPlus(newLit(1), newLit(2)), newLit(4)))

この例では、コンストラクタ newLitnewPlus は静的バインディングを使用するため proc ですが、 eval は動的バインディングを必要とするため method であることに注意してください。

Note: Nim 0.20 以降、マルチメソッドを使用するには、コンパイル時に明示的に --multimethods:on を渡す必要があります。

マルチメソッドでは、オブジェクト型を持つすべてのパラメータがディスパッチに使用されます。

type
  Thing = ref object of RootObj
  Unit = ref object of Thing
    x: int

method collide(a, b: Thing) {.inline.} =
  quit "to override!"

method collide(a: Thing, b: Unit) {.inline.} =
  echo "1"

method collide(a: Unit, b: Thing) {.inline.} =
  echo "2"

var a, b: Unit
new a
new b
collide(a, b) # output: 2

この例からわかるように、マルチメソッドの呼び出しはあいまいであってはならない。衝突の解決は左から右へ行われるので、衝突2が衝突1より優先される。したがって、 Unit, ThingThing, Unit よりも優先されます。

パフォーマンスノート: Nim は仮想メソッドテーブルを生成せず、ディスパッチツリーを生成します。これにより、メソッド呼び出しのための高価な間接分岐を回避し、インライン化を可能にします。しかし、コンパイル時の評価やデッドコードの除去など、他の最適化はメソッドでは機能しません。

例外について

Nimでは例外はオブジェクトです。慣習として、例外の型には ‘Error’ というサフィックスが付きます。system モジュールでは例外の階層を定義していますので、それに従った方が良いでしょう。例外は system.Exception から派生し、共通のインターフェイスを提供します。

例外は、その寿命が不明であるため、ヒープ上に確保する必要があります。コンパイラはスタック上に生成された例外を発生させないようにします。発生したすべての例外は、少なくとも msg フィールドに発生した理由を指定する必要があります。

慣習として、例外は 例外的 な場合に発生させるべきであり、制御フローの代替方法として使用すべきではありません。

Raiseステートメント

例外を発生させるには raise ステートメントを使用します。

var
  e: ref OSError
new(e)
e.msg = "the request to the OS failed"
raise e

もし raise キーワードの後に式が続かない場合は、最後の例外が 再レイズ されます。このよくあるコードパターンを繰り返さないようにするために、 system モジュールの newException というテンプレートを利用することができます。

raise newException(OSError, "the request to the OS failed")

Tryステートメント

try 文は例外を処理します。

from std/strutils import parseInt

# 数字を含むはずのテキストファイルの最初の2行を読み込んで、足し算をしようとする
var
  f: File
if open(f, "numbers.txt"):
  try:
    let a = readLine(f)
    let b = readLine(f)
    echo "sum: ", parseInt(a) + parseInt(b)
  except OverflowDefect:
    echo "overflow!"
  except ValueError:
    echo "could not convert string to integer"
  except IOError:
    echo "IO error!"
  except:
    echo "Unknown exception!"
    # 未知の例外を再認識する。
    raise
  finally:
    close(f)

try の後の文は、例外が発生しない限り実行されます。そして、適切な except の部分が実行される。

空の except 部分は、明示的にリストアップされていない例外が発生した場合に実行されます。これは if ステートメントにおける else パートに似ている。

もし、 finally パート があれば、それは常に例外ハンドラの後に実行されます。

例外は except パートの中で 消費 される。例外が処理されない場合、例外はコールスタックを介して伝搬される。つまり,finally節に含まれない残りの手続きは,(例外が発生しても)実行されないことが多いのです.

もし、exceptブランチの中の実際の例外オブジェクトやメッセージに アクセス する必要がある場合には、system モジュールの getCurrentException()getCurrentExceptionMsg() プロックを使用するとよいでしょう。

try:
  doSomethingHere()
except:
  let
    e = getCurrentException()
    msg = getCurrentExceptionMsg()
  echo "Got exception ", repr(e), " with message ", msg

例外を発生させたプロックに注釈を付ける

オプションの {.raises.} プラグマを使うことで、ある proc が特定の例外を発生させるか、あるいはまったく発生させないかを指定することができます。もし {.raises.} プラグマが使用されると、コンパイラはそれが正しいかどうかを検証します。例えば、ある proc が IOError を発生させると指定した場合、ある時点でその proc (またはその proc が呼び出すプロック) が新しい例外を発生させ始めたら、コンパイラはその proc をコンパイルできないようにします。

使用例

proc complexProc() {.raises: [IOError, ArithmeticDefect].} =
  ...
proc simpleProc() {.raises: [].} =
  ...

このようなコードを作成すると、発生した例外のリストが変更された場合、コンパイラはプラグマの検証を停止した proc の行と発生した例外をキャッチできなかった行、およびキャッチできなかった例外が発生したファイルおよび行を指定するエラーで停止するようになります。これは、変更された問題のあるコードを特定するのに役立つかもしれません。

もし、既存のコードに {.raises.} プラグマを追加したい場合は、コンパイラが手助けをしてくれます。proc に {.effects.} プラグマステートメントを追加すると、コンパイラはその時点までに推測されるすべての効果を出力します(例外の追跡は Nim の効果システムの一部です)。このコマンドはモジュール全体のドキュメントを生成し、すべての proc を例外の発生したリストで飾ります。Nim の エフェクトシステムと関連するプラグマについてはマニュアルを参照してください。

ジェネリックス

ジェネリックとは、プロックやイテレータ、型などを type parameters でパラメタライズするための Nim の手段です。ジェネリックパラメータは角括弧の中に Foo[T] のように記述されます。このパラメータは型安全なコンテナを効率的に作成するのに役立ちます。

type
  BinaryTree*[T] = ref object # BinaryTree は,一般的なパラメータ `T` を持つ
                              # 汎用型である.
    le, ri: BinaryTree[T]     # 左右の部分木; nilかもしれません。
    data: T                   # ノードに格納されているデータ

proc newNode*[T](data: T): BinaryTree[T] =
  # ノードのコンストラクタ
  new(result)
  result.data = data

proc add*[T](root: var BinaryTree[T], n: BinaryTree[T]) =
  # ツリーにノードを挿入する
  if root == nil:
    root = n
  else:
    var it = root
    while it != nil:
      # データ項目を比較します。`==` と `<` 演算子を持つ任意の型に
      # 対して動作する、汎用的な `cmp` プロシージャを使用します。
      var c = cmp(it.data, n.data)
      if c < 0:
        if it.le == nil:
          it.le = n
          return
        it = it.le
      else:
        if it.ri == nil:
          it.ri = n
          return
        it = it.ri

proc add*[T](root: var BinaryTree[T], data: T) =
  # コンビニエンスproc
  add(root, newNode(data))

iterator preorder*[T](root: BinaryTree[T]): T =
  # 二分木の前置走査。
  # これは明示的なスタックを使用します
  # (再帰的なイテレータファクトリよりも効率的です)。
  var stack: seq[BinaryTree[T]] = @[root]
  while stack.len > 0:
    var n = stack.pop()
    while n != nil:
      yield n.data
      add(stack, n.ri)  # 右のサブツリーをスタックに
      n = n.le          # プッシュし、左のポインタをたどる

var
  root: BinaryTree[string] # BinaryTreeを `string` でインスタンス化する。
add(root, newNode("hello")) # newNode` と `add` をインスタンス化する。
add(root, "world")          # 2番目の `add` procをインスタンス化します。
for str in preorder(root):
  stdout.writeLine(str)

この例では、一般的なバイナリツリーを示しています。文脈に応じて、括弧は型パラメータを導入するため、またはジェネリックな proc、iterator、または型をインスタンス化するために使われます。この例からわかるように、ジェネリックはオーバーロードと一緒に動作します。シーケンスのための組み込みの add 手続きは隠されておらず、 preorder イテレータの中で使用されます。

メソッドコールの構文でジェネリックスを使用する場合には、特別な [:T] 構文があります。

proc foo[T](i: T) =
  discard

var i: int

# i.foo[int]() # Error: expression 'foo(i)' has no type (or is ambiguous)

i.foo[:int]() # Success

テンプレート

テンプレートはNimの抽象構文木を操作する簡単な置換機構です。テンプレートはコンパイラのセマンティックパスで処理されます。テンプレートは言語の他の部分とうまく統合され、C言語のプリプロセッサ・マクロのような欠点はありません。

テンプレートを呼び出すには、プロシージャのように呼び出します。

template `!=` (a, b: untyped): untyped =
  # この定義はSystemモジュールに存在する
  not (a == b)

assert(5 != 6) # the compiler rewrites that to: assert(not (5 == 6))

演算子 !=, >, >=, in, notin, isnot は、実際にはテンプレートです。これは、 == 演算子をオーバーロードした場合に、 != 演算子が自動的に利用でき、正しい処理を行うという利点があります。(IEEE 浮動小数点数の場合は例外です。NaN は基本的なブーリアンロジックを破壊します。)

a > bb < a に変換されます。a in bcontains(b, a) に変換されます。notinisnot` は明らかな意味を持ちます。

テンプレートは遅延評価のために特に有用です。ロギングのための簡単なプロシージャを考えてみましょう。

const
  debug = true

proc log(msg: string) {.inline.} =
  if debug: stdout.writeLine(msg)

var
  x = 4
log("x has the value: " & $x)

このコードには欠点があります。いつか debug が false に設定された場合、かなり高価な $& の演算がまだ実行されてしまうのです! (プロシージャの引数評価は eager です)。

log プロシージャをテンプレート化することで、この問題を解決することができます。

const
  debug = true

template log(msg: string) =
  if debug: stdout.writeLine(msg)

var
  x = 4
log("x has the value: " & $x)

パラメータの型は、通常の型、またはメタ型である untypedtypedtype のいずれかを指定することができます。type は型付きシンボルのみが引数として与えられることを意味し、 untyped` は式がテンプレートに渡される前にシンボルのルックアップや型解決が行われないことを意味します。

テンプレートに戻り値の型が明示されていない場合は、プロックやメソッドとの一貫性を保つために void が使用されます。

テンプレートにステートメントのブロックを渡すには、最後のパラメータに untyped を使用します。

template withFile(f: untyped, filename: string, mode: FileMode,
                  body: untyped) =
  let fn = filename
  var f: File
  if open(f, fn, mode):
    try:
      body
    finally:
      close(f)
  else:
    quit("cannot open: " & fn)

withFile(txt, "ttempl3.txt", fmWrite):
  txt.writeLine("line 1")
  txt.writeLine("line 2")

この例では、2つの writeLine ステートメントが body パラメータにバインドされています。withFile テンプレートは定型的なコードを含んでおり、よくあるバグ、つまりファイルを閉じるのを忘れてしまうことを避けるのに役立っています。let fn = filename ステートメントが filename が一度だけ評価されることを保証していることに注意してください。

Example: Lifting Procs

import std/math

template liftScalarProc(fname) =
  ## スカラーパラメータを受け取りスカラー値を返す proc 
  ## (例 `proc sssss[T](x: T): float`) を持ち上げ,
  ## seq[T] や入れ子の seq[seq[]] あるいは同じ型の
  ## シングルパラメータを扱えるテンプレ式の proc を提供します.
  ##
  ## .. code-block:: Nim
  ##  liftScalarProc(abs)
  ##  # now abs(@[@[1,-2], @[-2,-3]]) == @[@[1,2], @[2,3]]
  proc fname[T](x: openarray[T]): auto =
    var temp: T
    type outType = typeof(fname(temp))
    result = newSeq[outType](x.len)
    for i in 0..<x.len:
      result[i] = fname(x[i])

liftScalarProc(sqrt)   # sqrt() がシーケンスに対して機能するようにする
echo sqrt(@[4.0, 16.0, 25.0, 36.0])   # => @[2.0, 4.0, 5.0, 6.0]

JavaScriptへのコンパイル

Nimのコードは、JavaScriptにコンパイルすることができます。しかし、JavaScript と互換性のあるコードを書くためには、以下のことを覚えなければなりません。

  • addrptr は JavaScript では意味的に少し異なる。もし、それらがどのようにJavaScriptに変換されるかわからない場合は、それらを避けることが推奨されます。
  • JavaScript の cast[T](x)(x) に翻訳されます。ただし、符号付き/符号なし int 間のキャストは例外で、その場合は C 言語の静的キャストと同じように動作します。
  • JavaScript における cstring は、JavaScript の文字列を意味します。意味的に適切な場合のみ cstring を使用するのがよいでしょう。例えば、バイナリデータのバッファとして cstring を使わないでください。

Part 3

次のパートでは、完全にマクロによるメタプログラミングについて説明します。パートIII