Nimについて (II)

はじめて

「ループは不条理を合理的にする」– Norman Wildberger

このドキュメントは、Nimプログラミング言語の高レベルの構造です。manualには高級言語機能の例が多く含まれているため、このドキュメントは少し古くなっていることに注意してください。

プラグマ(Pragmas)

プラグマは、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以外の新しいオブジェクトルートを導入できます。 (例えば、GTKパッケージはこの方法を使用します。)

継承を使用する限り、Refオブジェクトを使用する必要があります。必須ではありませんが、ref以外のオブジェクトの割り当ての場合、例えば、let person:Person = Student(id:123)はサブクラスフィールドを切り捨てます。

ヒント:単純なコードの再利用の場合、通常、コンポジション(has-a 関係)は継承(is-a関係)よりも優れています。オブジェクトはNimの値型であるため、合成は継承と同じくらい効果的です。

相互再帰型

オブジェクト、タプル、および参照は、相互に依存する非常に複雑なデータ構造をシミュレートできます。これらは相互に再帰的です。Nimでは、これらの型は単一の型セクションでのみ宣言できます。(つまり、任意のシンボルが必要なためにコンパイルが遅くなるその他の型です。)

実例:

type
  Node = ref object  # 以下のフィールドを持つオブジェクトへの参照:
    le, ri: Node     # 左右のサブツリー
    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でない場合は、InvalidObjectConversionError例外が発生されます。

オブジェクトバリアント

単純なバリアント型が必要な場合、通常、オブジェクト階層は過剰です。

実例:

# 以下のは、Nimで抽象構文木をモデル化する方法の実例である

type
  NodeKind = enum  # さまざまなノード型
    nkInt,          # 整数値リーフノード
    nkFloat,        # 浮動小数点リーフノード
    nkString,       # 文字列リーフノード
    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)
# 次のステートメントでは、n.kindの値が一致しないため、 `FieldError`例外が発生される:
n.strVal = ""

この実例から見ると、オブジェクト階層のメリットは、異なるオブジェクト型の間で変換する必要がないことです。ただし、無効なオブジェクトフィールドにアクセスすると、例外が発生されます。

メソッド呼び出し構文

ルーチンを呼び出すための糖衣構文があります。method(obj,args)の代わりに構文obj.method(args)を使用できます。 残りのパラメータがない場合は、括弧を省略できます:obj.len(l len(obj)ではない)。

このメソッド呼び出し構文はオブジェクトに限定されず、以下のすべての型に使用できます。

import strutils

echo "abc".len # is the same as echo len("abc")
echo "abc".toUpperAscii()
echo({'a', 'b', 'c'}.card)
stdout.writeLine("Hallo") # the same as writeLine(stdout, "Hallo")

(メソッド呼び出し構文を確認する別の方法は、欠落しているサフィックス表記を提供することです。)

そのため、「純粋なオブジェクト指向」コードは簡単に編集できます。

import 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-proceduresは同じです。ただし、設定値は異なり、特別なsetter文が必要です。

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

proc `host=`*(s: var Socket, value: int) {.inline.} =
  ## setter of host address
  s.h = value

proc host*(s: Socket): int {.inline.} =
  ## getter of host address
  s.h

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

(この実例では、inlineプログラムも示しています。)

[]配列アクセス演算子をオーバーロードして、配列属性を提供できます。

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 ## abstract base class for an expression
  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は動的バインディングを必要とするためメソッドであることに注意してください。

ヒント: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

実例が示すように、複数のメソッド呼び出しにあいまいがあることは許可しません。解析は左から右であるため、collide2はcollide1より人気です。そのため、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キーワードの後に式がない場合、最後の例外はre-raisedになります。この一般的なコードパターンの繰り返しを回避するために、systemモジュールでテンプレートnewExceptionを使用できます。

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

Tryステートメント

tryステートメントは例外を処理します。

from 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 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セクションを実行します。

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

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

except部分の例外を消費します。例外が処理されない場合、例外はコールスタックによって流れます。これは、プログラムの残りの部分はfinally節にないことを意味します-通常、実行されません。(例外が発生した場合)

*によってexceptブランチの実際の例外オブジェクトまたはメッセージにアクセスする必要がある場合は、systemモジュールのgetCurrentException()またはgetCurrentExceptionMsg()のプロセスを使用できます。例えば:

.. code-block:: nim

    try:
        doSomethingHere()
    except:

        let
            e = getCurrentException() msg = getCurrentExceptionMsg()

        echo "Got exception ", repr(e), " with message ", msg

例外を引き起こすprocsコメント

オプションの {.raises.} pragmaによって、プロシージャが特定の例外セットを発生させるか、例外がまったく発生しないかを指定できます。{.raises.} プラグマを使用する場合、コンパイラはこれが真であることを確認します。例えば、指定されたプロシージャがIOErrorを発生させ、ある時点でそれ(またはそれが呼び出すプロシージャ)が新しい例外を発生させる場合、コンパイラはプロシージャのコンパイルを阻止します。実例:

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

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

このようなコードを取得する場合、スローされた例外のリストが変更されると、コンパイラは停止し、プロセスがプラグマの行、キャッチされなかった例外とその行数、およびファイルの検証を停止することを指摘します。キャッチされない例外がスローされています。これは、変更された問題のあるコードを見つけるのに役立つ場合があります。

{.raises.} プラグマを既存のコードに追加する場合は、コンパイラも役立ちます。 {.effects.} プラグマステートメントをプロセスに追加すると、コンパイラはその時点までに推測されたすべてのエフェクトを出力します(例外追跡はNimのエフェクトシステムの一部です)。procによって発生した例外のリストを見つけるもう1つの方法は、モジュール全体のドキュメントを生成し、発生した例外のリストですべてのプロシージャを装飾するNim doc2コマンドを使用することです。Nimのエフェクトシステムと関連するプラグマの詳細については、マニュアルを参照してください。

ジェネリック

ジェネリックスは、型付きパラメータを使用してプロセス、イテレータ、または型をパラメータ化するNimのメリットです。それらは効率的な安全なコンテナに役立ちます:

type
  BinaryTree*[T] = ref object  # 二分木は、左右のサブツリーのジェネリック型のパラメータ `` T ''であり、nilの可能性がある
    le, ri: BinaryTree[T]
    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) =
  # プロセスを容易にする:
  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] #  `` string``を使用してバイナリツリーをインスタンス化する
add(root, newNode("hello")) # `` newNode``と `` add``をインスタンス化する
add(root, "world")          # 2番目の``add``プロセスをインスタンス化する
for str in preorder(root):
  stdout.writeLine(str)

この実例は、一般的な二分木を示しています。コンテキストに応じて、括弧は型パラメータを導入したり、一般的なプロシージャ、イテレータ、または型をインスタンス化するために使用されます。実例が示すように、ジェネリックはオーバーロードを使用します:「add」の最適な一致です。シーケンスの組み込みのaddプロシージャは非表示ではありませんが、preorderイテレータで使用されます。

レンプレート

テンプレートは、Nimの抽象構文ツリーで実行できる単純な置換メカニズムです。テンプレートは、コンパイラのセマンティック転送で処理されます。それらは他の言語とうまく統合されており、Cのプリプロセッサマクロの欠陥はありません。

テンプレートを呼び出し、それをプロセスとして使用します。

Example:

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

assert(5 != 6) # コンパイラはそれを以下のようにオーバーライドする:assert(not(5 == 6))

!=, >, >=, in, notin, isnot演算子はテンプレートです。これは、自動的に使用できる== , !=演算子をオーバーロードするのに適しています。(IEEE浮動小数点数を除く-NaNは基本的なブール論理を破ります。)

a > bb < aに変換されます。a in bcontains(b, a) に変換されます。notinisnotは、名前が示すとおりです。

テンプレートは、計算の遅延に特に役立ちます。簡単なロギングプロセスをご覧ください。

const
  debug = true
const
  debug = true

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

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

このコードには欠点があります。ある日デバッグがfalseに設定されている場合でも、$および操作は実行されます。(プログラムのパラメータ評価は至急です)

logプロセスをテンプレートに変換すると、この問題が解決します。

const
  debug = true

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

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

パラメータの型は、通常の型にすることか、untypedtyped、またはtypeにすることができます。 typeは、パラメータとして指定できる型シンボルが1つだけであることを意味し、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が1回だけ評価されることを保証する方法に注意してください。

実例:リフティングプロセス

import math

template liftScalarProc(fname) =
  ## スカラーパラメータを使用してprocをプロモートし、
  ## スカラー値を返し、(例えば、 ``proc sssss[T](x: T): float``),
  ## 単一のseq[T]パラメータまたはネストされたseq[seq[]]または同じ型を処理できるテンプレートプロセスを提供する
  ##
  ## .. code-block:: Nim
  ##  liftScalarProc(abs)
  ##  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)   # 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) に変換されます。この場合、C言語では静的キャストとして表示されます。
  • cstringはJavaScriptのJavaScript文字列を表します。cstringは、意味的に適切な場合にのみ使用することをお勧めします。例えば。 cstringをバイナリデータバッファとして使用しないでください。
Share

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です