LoginSignup
16
7

More than 3 years have passed since last update.

Nimチュートリアル Part2 要点まとめ

Last updated at Posted at 2019-08-10

NimチュートリアルPart2

NimチュートリアルPart2の個人的な要点まとめ
Ver0.20.0

Pragmas

オブジェクト指向

  • 継承
    • 完全にオプショナルな機能
    • 多重継承は現時点では未サポート
  • 継承させたいものはRootObj を継承している必要がある。
    • object ofシンタックス
  • 継承を伴う型は、厳密には強制されてないが、普通はrefとしてマークされる。
    • 実行時にオブジェクトが特定の型であるかどうかを確認するにはof演算子を使用する
  • 継承元がないobjectfinal扱い
type
  Person = ref object of RootObj
    name*: string 
    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[]
  • 基本的に継承されるオブジェクトは ref だが強制ではない
    • ただし non-refな型で let person: Person = Student(id:123) とかすると、サブクラスフィールドが全部削ぎ落とされる。

相互再起型

  • 一つの type 宣言のなかで定義する
type
  Node = ref object  # a reference to an object with the following field:
    le, ri: Node     # left and right subtrees
    sym: ref Sym     # leaves contain a reference to a Sym

  Sym = object       # a symbol
    name: string     # the symbol's name
    line: int        # the line the symbol was declared in
    code: Node       # the symbol's abstract syntax tree

型変換

  • 型キャストと型変換は明確に区別する
    • キャストは cast 演算子を使う
    • コンパイラは別の型のビットパターンを受け取るようになる
  • 型変換はAST的な値を維持したまま変換し内部のbit表現は変化は必須ではない
    • 型変換が不可能な場合、コンパイル警告か例外が発生する
    • InvalidObjectConversionError
    • 型変換は 変換したい型名(値) の形式で行う。

オブジェクトバリアント

  • オブジェクトの継承関係よりバリアントがシンプルで良い場合がある
# Nim の抽象構文木をどうモデリングするかの例
type
  NodeKind = enum  # AST のノードを表す
    nkInt,          # 整数の AST Node
    nkFloat,        # Float の AST Node
    nkString,       # 文字列の AST Node
    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にfitする値ではないので `FieldError`例外が発生する
n.strVal = ""
  • オブジェクト階層の利点
    • 異なるオブジェクトタイプ間の変換が必要ない
    • 無効なオブジェクトフィールドにアクセスすると例外が発生する

メソッド呼び出しのシンタックス

  • メソッド呼び出しには糖衣構文が用意されている
    • method(obj, args) → obj.method(args)
    • 引数なしの場合は ()省略可能で obj.method と記述可能
    • この糖衣構文はオブジェクト以外にも適用可能
import strutils

echo "abc".len  # len("abc") と書くのと同じ
echo "abc".toUpperAscii()
echo({'a', 'b', 'c'}.card)
stdout.writeLine("Hallo") # 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!")

プロパティ

  • メソッド呼び出しの糖衣構文でオブジェクトプロパティの getter は必要ないのでは?となるが
    • しかし setter に相当するものはない
    • よって新しい構文が必要になる
type
  Socket* = ref object of RootObj
    h: int # `*` がついてないので外部に露出していない

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

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

var s: Socket
new s
# 定義した演算子でsetterを実現する
s.host = 34  # same as `host=`(s, 34)

動的ディスパッチ

  • プロシージャは常に static dispatch
  • 動的ディスパッチは 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.} =
  # この method をオーバーライドする
  quit "to override!"

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

# 以下のコンストラクタは proc で static binding のが理にかなっている
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)))

multi-method

  • オブジェクトタイプを持つすべてのパラメーターがディスパッチに使用できる
    • 注意: multi-method使用時はコンパイラに --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
# (unit, unit) の引数なので (Unit, Thing) が呼び出される
collide(a, b) # output: 2

パフォーマンスの注意

  • Nimは仮想methodテーブル(いわゆるvtable)を生成しないが、メソッドの呼び出しツリーを生成する
    • メソッド呼び出しの高コストな間接分岐が回避され、インライン化が可能
    • しかし method はコンパイル時計算やデッドコード除去など、他の最適化はできない

例外処理

  • Nimの例外はオブジェクト
    • system.Exception から派生する
    • 慣習としてErrorが末尾につく
    • msgフィールドで詳細情報を書くのが必須
  • 例外は生存期間が不明なので常にヒープ領域に置かれる
  • Nimでの慣習として
    • 例外は本当に例外的なときだけ発生させるようにしましょう
    • 例えばFileが開けないときに例外は起きない
      • ファイルがない、というのはよくある一般的な状況で 例外 ではないよね?ということ

Raise文

  • raiseが単体で使われる場合、前回と同じ例外が投げられる
    • これを避けるため、system の以下のテンプレを使う
    • raise newException(OSError, "the request to the OS failed")
var
  e: ref OSError
new(e)
e.msg = "the request to the OS failed"
raise e

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)
  • 例外オブジェクトはexcept消費される
    • 消費されなかった場合スタックを通じて例外が伝搬する
      • finallyを除く、残りのプロシージャ部分が実行されなくなる
  • 現在の例外オブジェクトにアクセスする
    • systemgetCurrentException, getCurrentExceptionMsg を使う
try:
  doSomethingHere()
except:
  let
    e = getCurrentException()
    msg = getCurrentExceptionMsg()
  echo "Got exception ", repr(e), " with message ", msg

例外でプロシージャにアノテーションを加える

  • {. raises .}プラグマを使う
    • このプラグマの指定外の例外が発生するコードがあるとコンパイルが止まる
    • 想定外の変更をコンパイラが通知してくれて便利な場合がある
  • {. effects .} プラグマを追加すると、そのプロシージャの影響(例外含む)を出力してくれるので例外把握に便利。
proc complexProc() {.raises: [IOError, ArithmeticError].} =
  ...

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

ジェネリクス

  • 型安全なコンテナ
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

テンプレート

  • NimのASTで動く、かんたんな置換メカニズム
    • テキストのプリプロセスに近い。
    • Nim本家曰く言語とうまく統合され、Cのプリプロセッサマクロより便利
template `!=` (a, b: untyped): untyped =
  # systemモジュールに実際に定義されている例
  not (a == b)
  assert(5 != 6) # コンパイラが右のように書き換える assert(not (5 == 6))
  • Nimの!=, >, >=, in, notin, isnotは実際にはテンプレート
    • 例えば == の演算を定義するとテンプレートにより自動的に != も使えるようになるという利点がある

テンプレートは遅延評価をしたいときに特に便利

  • 以下の例は debug = false でも log() の呼び出し時の $ などの処理が走ってしまい、無駄な演算コストがかかる
const
  debug = true

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

var
  x = 4
log("x has the value: " & $x)
  • これをテンプレート化(上記コードの proctemplate に変える)すると無駄な処理を省ける
    • log の部分が愚直に if debug: .. に置き換わるので、引数の評価が実行されない

テンプレートと型

  • untyped
    • テンプレートに式が渡される前にシンボル検索と型解決が実行されない
    • ブロックをテンプレートに渡したいときは、最後の引数に untyped を追加する
  • typed
  • type
    • 型シンボルのみ引数に与えられる
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):
  # 以下の2秒は body パラメータに渡される
  txt.writeLine("line 1")
  txt.writeLine("line 2")
  • 明示的な返り型のないテンプレートは void になる

テンプレートでのプロシージャ上書き

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 へのコンパイル

  • jsへのコンパイルできるコードを書くうえで気にかけること
  • addr, ptr のは JavaScript 上ではセマンティクスが異なる
    • jsコンパーチブルにしたければ addr, ptr の使用を避けるのが吉
    • jsへのコード変換がどのようにされるか把握しているなら使用OK
  • cast[T](x)jsでは (x) にトランケイトされる
    • 符号付き/符号なしint間のキャストは例外
    • C言語では静的キャストとして動作
  • cstringjsの文字列を意味する。
    • cstringはセマンティクス上適切なときのみ使うのが良い慣習
    • バイナリデータ用のバッファとかでcstring使うのは良くない
16
7
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
16
7