14
10

More than 5 years have passed since last update.

Nim Tutorial Part IIを日本語訳してみた

Last updated at Posted at 2019-03-18

本家チュートリアルのパート2を日本語訳していきます。自分の勉強がてら。
KTakahiro1729さんの書いたPart Ⅰ日本語訳はこちら

  • オリジナルの著者:Andreas Rumpf
  • nimのバージョン: 0.19.4

はじめに

"繰り返すことは馬鹿げたことを合理的にする" -- ノーマン・ワイルドバーガー

この文書はNimの高度な構文に対するチュートリアルです。この文書はより多くの高度な言語機能の例を含むマニュアルとして使えるものではないことに気をつけてください。

プラグマ(Pragma)

プラグマは多くの新しいキーワードを導入することなくコンパイラに追加情報やコマンドを与えるNimの手法です。プラグマは特別なカッコである{..}というようなカーリードットブラケットで囲まれて表現されます。 このチュートリアルではプラグマは扱いません。最新のプラグマの詳細はユーザーガイドかマニュアルをご覧ください。

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

Nimのオブジェクト指向プログラミング(OOP)へのサポートは最低限である一方で、強力なOOPのテクニックを使うことができます。 Nimはプログラムを設計する一つの方法であるものの、唯一の方法であるわけではありません。手続き的なアプローチのほうがよりシンプルで、より効率的になることもあるでしょう。例えば継承よりもコンポジションパターンを好むほうがよりよいデザインになることもあるでしょう。

オブジェクト

タプルのように、オブジェクトというものは、構造化された方法で異なる値を一緒にまとめることを意味します。しかしながら、オブジェクトはタプルが持たない多くの機能を提供します:

  • 継承と情報の隠蔽を提供します。オブジェクトはデータをカプセル化するので、T()というオブジェクトを作る関数は内部的に使われるだけであり、プログラマはオブジェクトを初期化する関数(Proc)を提供するだけで良いのです。 (これをコンストラクタと呼びます。)

オブジェクトは実行時にその型にアクセスできます。オブジェクトの型を確認するのに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) # true
# object 作成:
student = Student(name: "Anton", age: 5, id: 2)
echo student[]

定義したモジュールの外から見れるオブジェクトフィールドは*でマークされます。タプルとは違って、異なるオブジェクトの型は等価になることはありえません。新しいオブジェクトはtypeセクションの中でだけ定義されます。

継承はオブジェクトのof構文により成されます。多重継承は現在サポートされていません。もしあるオブジェクトが適切な祖先を持たない場合、RootObjが祖先となりえますが、これは慣例的なものです。祖先を持たないオブジェクトは最後であることを暗示します。system.RootObjの代わりに継承可能なプラグマを、新しいオブジェクトを導入する際に使うことができます。(GTKの継承のラッパーに使われています。)

Refオブジェクトは継承が使われているときいつでも使えます。厳密には必要ではありませんが、let person: Person = Student(id: 123)のようなrefのないオブジェクトはサブクラスのフィールドを切り捨てられます。

Note: has-aの関係を持つコンポジションは同じようにhas-aの関係を持つ継承よりもシンプルな再利用性のために好まれます。Nimではオブジェクトは値をもつ型なので、コンポジションは継承と同じぐらい効果的なのです。

相互再帰的な型(Mutually recursive type)

オブジェクトやタプルや参照はとても複雑なデータ構造を作ることができ、相互に依存させることができます。相互再帰的な型なのです。Nimではこれらの方は一つのtypeセクション中でのみ宣言されます。 (他の書き方では任意のシンボルの先読み(arbitary symbol lookahead)を必要とし、コンパイルを遅らせることになります。)

例:

type
  Node = ref object  # 以下のフィールドをもつオブジェクトへの参照:
    le, ri: Node     # 左右の部分木
    sym: ref Sym     # 葉に`Sym`への参照を持つ

  Sym = object       # シンボル
    name: string     # シンボルの名前
    line: int        # シンボルが宣言された行
    code: Node       # シンボルの抽象構文木

型変換

Nimは型のキャストと型の変換を区別します。キャストはキャスト演算子によって成され、コンパイラにビットの配列パターンを他の型であるように強制的に解釈させます。

型変換は型のキャストより丁寧な手法です。型変換では抽象的な値が保持され、ビットのパターンを用いる必要はありません。もし型変換が不可能であった場合、コンパイラはそれを指摘するか、例外を発生させます。
型変換の構文は、目的の型(変換の表現)(通常の呼び出しのように)です。:


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

xが生徒ではなかったときに InvalidObjectConversionError 例外が発生します。

オブジェクトの階層構造

オブジェクトの階層構造はシンプルなバリアント型が必要な場合に「やり過ぎ」てしまう場合があります。

例:

# これはNimで組まれうる抽象構文木の例です。
type
  NodeKind = enum  # 異なるノードの型
    nkInt,          # integerの値をもつ葉
    nkFloat,        # floatの値をもつ葉
    nkString,       # stringの値をもつ葉
    nkAdd,          # 足し算
    nkSub,          # 引き算
    nkIf            # if文
  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)
# 次の文は`FieldError` 例外を発生させます。なぜなら
# n.kindの値が合わないからです。:
n.strVal = ""

例からもわかるように、オブジェクトの階層構造の利点は異なるオブジェクトの型を変換しないでよいということです。一方で、不正なフィールドへのアクセスは例外を発生させます。

メソッド

一般的なオブジェクト指向言語はプロシージャ(メソッドとも言う)はクラスに束縛されています。これは不利な点です。:

プログラマが制御できないクラスにメソッドを追加することは不可能であるか、または醜い回避策が必要です。
メソッドをどこに配置すべきかは明確でないことがよくあります。: joinstringメソッドでしょうか、arrayのメソッドでしょうか。
Nimはこれらの問題を、クラスにメソッドを割り当てないことで回避しました。
Nimにおけるすべてのメソッドはマルチメソッドです。下記に示すように、マルチメソッドは動的バインディングの目的でのみプロシージャと区別されます。

メソッド呼び出しの構文

ルーチンを呼び出すには糖衣構文があります。: obj.method(args)という構文はmethod(obj, args)の代わりに使うことができます。実引数が残らない場合はカッコを省略できます。:len(obj)の代わりにobj.len

メソッド呼び出し構文はオブジェクトには縛られておらず、どのような型についても使えます。:


import strutils

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

(メソッド呼び出し構文は後置される要素が省略されうというふうにみなすこともできます。)
なので、「完全オブジェクト指向」な書き方は簡単にかけます:


import strutils, sequtils

stdout.writeLine("数字のリストを入力してください。 (スペース区切り): ")
stdout.write(stdin.readLine.splitWhitespace.map(parseInt).max.`$`)
stdout.writeLine(" が最大です")

プロパティ

下の例が示すように、nimはgetプロパティを必要としません。: 通常のgetプロシージャはメソッド呼び出し構文と同じように呼び出せます。しかし、値をセットするのは少し異なります。このため、セッターには特別な構文が必要です。:


type
  Socket* = ref object of RootObj
    h: int # *がないため、モジュールの外からアクセスすることはできない

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

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

var s: Socket
new s
s.host = 34  # same as `host=`(s, 34)

(例はインラインプロシージャでもあります。)

[]アクセス演算子はarrayプロパティを与えることでオーバーロードできます。:


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)

この例はひどいものです。なぜならvectorv[]でアクセスできるタプルでモデル化したほうが良いからです。

動的ディスパッチ

プロシージャはいつも静的に発行されます。
動的に発行するにはprocmethodキーワードに置き換えれば良いです。:


type
  Expression = ref object of RootObj ## `expression`のための抽象クラス
  Literal = ref object of Expression
    x: int
  PlusExpr = ref object of Expression
    a, b: Expression

# 注意してください: 'eval'は動的バインディングに依存します
method eval(e: Expression): int =
  # override this base method
  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はメソッドです。

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


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

例が示すように、マルチメソッドの呼び出しはあいまいにすることはできません。Collide 2Collide 1より優先されます。解決は左から右に動作するためです。したがって、Unit、ThingはThing、Unitよりも優先されます。

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

例外

Nimでは例外はオブジェクトです。慣例により、例外タイプの末尾には「Error」が付きます。システムモジュールは例外階層を定義します。例外は、共通のインタフェースを提供するsystem.Exceptionから派生します。

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

規約としては、例外的なケースにのみ例外が発生されるべきということがあります。たとえば、ファイルを開くことができない場合、これは非常に一般的であるため、例外を発生させることはできません(ファイルが存在しない場合があります)。

Raise statement

例外の発生は、raise文により成されます。:


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

raiseキーワードの後に​​式が続かない場合は、最後の例外が再度発生します。この共通コードパターンの繰り返しを避けるために、システムモジュール内のテンプレートnewExceptionを使用することができます。

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

try文で例外を扱います。:


from strutils import parseInt

# 数値を含んでいると想定されるテキストファイルの最初の二行を読み込む
# そしてそれらを足そうとする
var
  f: File
if open(f, "numbers.txt"):
  try:
    let a = readLine(f)
    let b = readLine(f)
    echo "sum: ", parseInt(a) + parseInt(b)
  except OverflowError:
    echo "overflow!"
  except ValueError:
    echo "could not convert string to integer"
  except IOError:
    echo "IO error!"
  except:
    echo "Unknown exception!"
    # reraise the unknown exception:
    raise
  finally:
    close(f)

try以下の文は例外が発生しない場合にのみ実行されます。そして適切な例外が実行されます。

明示的にリストされていない例外がある場合は、空のexcept部分が実行されます。 if文のelse部分と似ています。

finally部分がある場合は、常に例外ハンドラの後に実行されます。

例外はexcept部分で消費されます。例外が処理されない場合は、コールスタックを通じて伝播されます。これはつまり、(例外が発生した場合)プロシージャの残りの部分(finally節内にない)が実行されないことを意味します。

exceptブランチ内の実際の例外オブジェクトまたはメッセージにアクセスする必要がある場合は、システムモジュールからgetCurrentException()およびgetCurrentExceptionMsg()プロシージャを使用できます。例:

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

発生する例外の注釈をprocsに付ける (Annotating procs with raised exceptions)

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


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

proc simpleProc() {.raises: [].} =
  ...

このようなコードを書けば、発生した例外のリストが変更された場合、コンパイラは次のようなエラーを出して停止します。

  • プラグマの検証を停止したprocの行
  • 発生したが補足されなかった例外
  • およびその例外をもつファイルと行

これらは変更された問題のあるコードを見つけるのに役立ちます。

既存のコードに{.raises.}プラグマを追加したい場合は、コンパイラが役に立ちます。
{.effects.}プラグマステートメントをprocに追加すると、コンパイラはそれまでに推定されたすべてのeffectを出力します(例外追跡はNimのeffectシステムの一部です)。

procによって発生した例外のリストを見つけるためのもう1つのより間接的な方法は、モジュール全体のドキュメントを生成し、発生した例外のリストですべてのprocを装飾するNim doc2コマンドを使用することです。マニュアルでNimのエフェクトシステムと関連プラグマについてもっと読むことができます。

ジェネリクス(Generics)

Generics are Nim's means to parametrize procs, iterators or types with type parameters. They are most useful for efficient type safe containers:

ジェネリクスは、型パラメータを使用してproc、イテレータ、または型をパラメータ化するためのNimの手段です。それらは便利な型安全なコンテナに最も有用です:


type
  BinaryTree*[T] = ref object # BinaryTree is a generic type with
                              # generic param ``T``
    le, ri: BinaryTree[T]     # left and right subtrees; may be nil
    data: T                   # the data stored in a node

proc newNode*[T](data: T): BinaryTree[T] =
  # constructor for a node
  new(result)
  result.data = data

proc add*[T](root: var BinaryTree[T], n: BinaryTree[T]) =
  # insert a node into the tree
  if root == nil:
    root = n
  else:
    var it = root
    while it != nil:
      # compare the data items; uses the generic ``cmp`` proc
      # that works for any type that has a ``==`` and ``<`` operator
      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) =
  # convenience proc:
  add(root, newNode(data))

iterator preorder*[T](root: BinaryTree[T]): T =
  # Preorder traversal of a binary tree.
  # Since recursive iterators are not yet implemented,
  # this uses an explicit stack (which is more efficient anyway):
  var stack: seq[BinaryTree[T]] = @[root]
  while stack.len > 0:
    var n = stack.pop()
    while n != nil:
      yield n.data
      add(stack, n.ri)  # push right subtree onto the stack
      n = n.le          # and follow the left pointer

var
  root: BinaryTree[string] # instantiate a BinaryTree with ``string``
add(root, newNode("hello")) # instantiates ``newNode`` and ``add``
add(root, "world")          # instantiates the second ``add`` proc
for str in preorder(root):
  stdout.writeLine(str)

例は一般的な二分木を示しています。文脈に応じて、角括弧[ ]は、型パラメータを導入するため、または一般的なprociterator、またはtypeをインスタンス化するために使用されます。例が示すように、ジェネリックスはオーバーロードを伴って動作します: 最もよくマッチしたaddが使われるわけです。シーケンスの組み込み追加手続きは隠されておらず、preorderイテレータで使用されます。

テンプレート

テンプレートはNimの抽象構文木を扱うシンプルな代替メカニズムです。テンプレートはコンパイラのセマンティック処理中に処理されます。テンプレートによって言語は拡張され、Cのプリプロセッサの欠点は共有されないようになっています。

テンプレートを実行するにはプロシージャを呼ぶように使います。

例:


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

assert(5 != 6) # コンパイラはこれをこのように書き換える: assert(not (5 == 6))

!=,>,>=, in, notin, isnotといった演算子は実際にテンプレートで実装されています。: これはもし、==演算子をオーバーロードしたときには!=演算子自動的に使えるようになり、正しく動くようになるというメリットがあります。(IEEEの浮動小数点数 - NaNだけ例外で通常のbooleanの挙動にはなりません。)

a > bb < aに、a in bcontains(b, a)に変換されます。notin and isnotは明確な意味を持ちます。

テンプレートは特に遅延評価の目的において便利です。ログのための簡単なプロシージャを例に考えてみましょう。:


const
  debug = true

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

var
  x = 4
log("x は次の値です: " & $x)

このコードは欠点があります。: もしdebugfalseになっても、計算量の大きな演算子である$&演算子は動き続けるのです! (実引数への評価は先行評価なのです).

ログのプロシージャをテンプレートに変えることでこの問題は解決されます。:


const
  debug = true

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

var
  x = 4
log("x は次の値です: " & $x)

仮引数の方はordinary型か、meta型であるuntypetypedtypeです。 型推論は実引数より与えられる型のシンボルで行われ、untypedはシンボル探索と型分析がテンプレートに式がわたる前に実行されていないということを締めします。

もしテンプレートが曖昧さのない型を返すのであれば、プロシージャやメソッドとの一貫性のためにvoidが使われます。

テンプレートのステートメントのブロックを通すには、最後の仮引数にuntypedを使ってください:


template withFile(f: untyped, filename: string, mode: FileMode,
                  body: untyped): typed =
  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文がファイル名を一度だけ確かめている方法に注意してみてください。.

例: リフティングプロシージャ


import math

template liftScalarProc(fname) =
  ## Lift a proc taking one scalar parameter and returning a
  ## scalar value (eg ``proc sssss[T](x: T): float``),
  ## to provide templated procs that can handle a single
  ## parameter of seq[T] or nested seq[seq[]] or the same type
  ##
  ## .. 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 = type(fname(temp))
    result = newSeq[outType](x.len)
    for i in 0..<x.len:
      result[i] = fname(x[i])

liftScalarProc(sqrt)   # make sqrt() work for sequences
echo sqrt(@[4.0, 16.0, 25.0, 36.0])   # => @[2.0, 4.0, 5.0, 6.0]

JavaScriptへのコンパイル

NimはJavaScriptにコンパイルできます。しかしながらJavaScriptに適合したコードを書くには以下のことに気をつけてください:

  • addrptr はJSにおいて少し異なる意味を持ちます。もしJSにどのように翻訳されるのか、詳しくない場合にはこれらのキーワードを避けたほうが望ましいでしょう。
  • cast[T](x) はJSでは(x) と変換されますが、unsigned int型とsigned int型の間の変換はC言語における静的なキャストと同じように振る舞います。
  • cstringはJSではstringを意味します。意味論的に適切な場合にのみcstringを用いることが良いプラクティスとなります。 例えば、バイナリのバッファとしてcstringを使ってはいけません。

Part 3

次のパートではマクロを使ったメタプログラミングについて扱います: Part III

訳注

正直全然わからない文がかなり存在していたので、フィーリングで書いてある部分が多いです。
色々間違ってると思うのですがご指摘ください。
Nimの学習を続けていくうちにあーこれは誤訳だなと思われる箇所を見つけたら逐次修正していきます。

TODO

  • 本家ドキュメントへのリンクを追記
14
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
10