LoginSignup
36
24

More than 3 years have passed since last update.

Nim ざっくり勉強してみたまとめ

Last updated at Posted at 2019-12-18

チュートリアルを読み切ったので自分用メモ。競プロと相性良さそうだし使ってみようかなと思って勉強したら AtCoder の Nim めっちゃ古くて泣いた。でも面白そうな言語。 C にトランスパイルされる Lisp に Python の皮を被せたみたいな趣を感じる。演算子周りは Haskell っぽい風情もある。

今時の言語らしくいろいろ便利記法が用意されてて、実用的っぽい。やたらシンタックスシュガーが多い言語は個人的には好きじゃないけど、これくらいならアリかも。インデントでブロックを作るのはまあ、そこは時代の流れっぽい。

書きやすさ、安全さ、実行効率のバランスの取り方がかなり独特で面白い(for 文にナチュラルにループのアンロール回数を指定する記法とか生えてるのやばない?)。こんなふわっと書けて C と同等に速いのはすごい。言語仕様を見てるとヒープにアロケートされるオブジェクトがなるべく少なく済むように意識した設計になっていそうで、その辺が速さに効いてるのかな (ヒープにアロケートすると GC 管轄になる)。ライフタイムなどをすべて管理する Rust もカッチリしていて綺麗だけど、これはこれで良い感じの落とし所っぽく見える。中間コードが human readable な C なので、デバッグでいきなりバイトコードまで潜らなくていいのも良さげ。

大半のビルトインが暗黙にロードされる system モジュールとして nim 自体で実装されている (self-contained) らしい。 hackable だし、問題があった時にソースに当たりやすいし、コアも小さくなるので良い方針だと思う。ドキュメントを書くためのコメント構文とかも用意されてて、この辺は lisp 系っぽい感じがする。同図象性も入っていて、構文木をゴリゴリやる系のマクロも書ける。さすがに lisp よりはしんどいけど…。

以下、概ね Nim 公式の "Nim Tutorial (Part I - III)" の超ざっくり要約。

I/O

  • stdin からメソッドが生えてる
let s = stdin.readLine # 一行読み込む
  • 競プロ的には strutilssplitsequtilsmap と併用することが多そう
import strutils, sequtils

let lst = stdin.readLine.split.map(parseInt) # 整数のリストが得られる
  • echo はなんでもいい感じに出力 (改行付き)
echo 30

変数

  • var はミュータブル、 let はイミュータブル、 const はコンパイル時定数 (!!)

  • 型か初期値の少なくとも一方が必須 (型だけを与えると「その型のデフォ値」がセットされる (!!))

var hoge: int # 0 になる
let fuga = 1
const piyo : string = stdin.readLine # コンパイル時に確定しないのでエラー
  • 一括初期化もできるけど、 最適化のために右辺の式はアンロールされて複数回呼ばれることがあるので副作用に注意 (!!)
var
  x, y : int
  a, b, c = "hoge" # a, b, c は全部 hoge になる

この辺のアグレッシブな感じ尖ってて好き。ここでハマったら半日くらい帰ってこれなさそう。

分岐

  • 条件は括弧で囲む必要なし

  • elif

if hoge == 1:
  echo "hoge"
elif hoge == 2: # else: if: だとインデントややこしいので elif
  echo "fuga"
else:
  echo "piyo"
  • case もある (パターンマッチというよりは C の switch 文っぽい?)

  • パターンが期待されているところに range っぽいものを書くといい感じになる

case n # ここには ':' がつかないっぽい
of 1:
  echo "1"
of 2..4, 7..9:
  echo "2~3 or 7~9" # 範囲も使える
else:
  discard # 使わない場合は明示的に discard

静的な網羅性チェックがあるので else 忘れるとコンパイルエラーにしてくれる。

  • when はコンパイル時に静的に展開される (#ifdef みたいな感じ)
when system.hostOS == 'windows': # 普通に nim の式が書ける (!!)
  echo "windows!!"

ループ

  • while は普通
while hoge == "":
  hoge = stdin.readLine
  • for<var> in <iterator> の形式オンリーっぽい
for i in countup(1, 10):
  echo i
  • iterator が期待されている場所に range っぽい記法を使うといい感じになる
for i in 10..1: # countdown(10, 1) と同じ
  echo i
for i in 1..<8: # < をつけると exclusive になる
  echo i
  • コレクションにはだいたい items, pairs が生えていて iterator を取れる
    • しかも省略可っぽい…?あとでそういう例が出てきた。もしかすると seq 限定かも
for i in [1, 2, 3].items:
  echo i
for (ix, i) in [1, 2, 3].pairs:
  echo (ix, i)

その他の制御

  • break, continue が普通にある

  • block はスコープを作る他に、名前をつけて大域脱出にも使える

block hoge: # 名前がつけられる
  while hoge:
    while fuga:
      break hoge # 名前を指定して break できる

改行ルール

  • 単一の代入文、関数呼び出し、 return 文は改行を省略できる
if hoge: echo "hogehoge!!"
  • それ以外 (if, when, for, while などのネスト) は改行必須
if hoge:
  if fuga: echo "hogefuga!!"
  • 式の途中は自由に改行してよい
if hoge(
     1, 2, 3,
     4, 5, 6
   ) and fuga (
     7, 8, 9
   ):
  echo "hogefuga!!"
  • 文を ; で区切って括弧に放り込むと、値を返す式として使える
const hoge = (var x = 0; for i in 1..10: x += i; x); # しかもコンパイル時計算!
  • 極端な例では、関数呼び出しの最後の引数をブロックで計算してしまうこともできる
echo "Hello ":
  var bikkuri = '!'
  "Wor" & "ld" & $bikkuri

関数

  • proc で定義、型は手書き

チュートリアルには登場しなかったけど実は auto もあるっぽい

  • 値の返し方がいろいろある
proc hoge (foo: int): bool =
  foo == 2 # 最後に評価した式が返る


proc fuga (foo: int): bool =
  if foo < 4: return false; # return で先に返す
  true

proc piyo (foo: int): int =
  for i in 1..foo:
    result += i # result という変数が暗黙に定義されるので、そこに結果を貯める

result 強烈だけど便利そう。「最後に評価した式が返る」パターンと「result が勝手に返る」パターンがコンフリクトしそうに見えるけど、 result 変数を一度も使っていなければ最後に評価した式が返るっぽい。

  • 引数に var をつけると関数側からその変数を書き換えられる
proc assign (foo: int, bar: var int) =
  bar = foo

var hoge;
assign(2, hoge) # hoge に 2 が入る

フィールドもいじれるっぽいので lisp の setf みたいな仕組みが入っていそう。 hoge[2] = 3 とか書いたときの [] 演算子も同じような仕組みになってるんだろうか。この辺はエレガントだなあ

  • 第一引数を外に出してメソッドチェインみたいに書ける。1引数関数の場合 () は省略可
proc addOne (foo: int) =
  foo + 1

echo 2.addOne # addOne(2) と同じ意味

Perl っぽい。とても好き

  • 戻り値を捨てる場合は明示的に discard
proc hoge () : int =
  echo 2
  100

discard hoge() # 副作用だけが欲しい場合は明示的に discard しないとエラー
proc hoge () = # 戻り値の型を省略すると値を返さない関数になるっぽい
  echo "hoge!!"

hoge()
proc hoge () : int {.discardable.} = # 値を返しつつ pragma で discard を省略可にもできる
  echo 2
  100

hoge()

C とかだと (void) つけて警告消したりするけど、このほうがよっぽど良さそう。

  • default value 付きの引数とか名前付き引数もいける。現代的
proc hoge (foo: int, bar: int = 3) : bool =
  foo + bar

echo hoge(foo = 2)
  • 型ごとにオーバーロードすると静的型を見て適切にディスパッチしてくれる
proc foo (foo: int) =
  echo foo + 1

proc foo (foo: string) =
  echo "string"
  • 相互再帰などでは C 言語のように forward declaration が必要 (将来なくなる可能性アリらしい)
proc hoge (foo: int) : int
  • 関数の型もまとめて書けるっぽい
proc hoge (foo, bar: int; baz: string) : int # まとめて書く場合はセミコロンで区切るっぽい

演算子

  • 演算子も全て関数、バッククォートで囲むとユーザー定義もできる
proc `+?` (foo: int, bar: int) : int =
  foo + bar + 1

echo 3 +? 2

echo `+?`(1, 2) # 関数のシンタックスでも呼び出せる

演算子に使える文字は +-*\/<>=@$~&%!?^.| のみ。組み込みの and, or, not などは特別枠。

1引数にすれば prefix、2引数にすれば infix。 postfix は原則ナシ

  • 配列などのインデックス参照 ([]) も特別なオペレータとして実装されているっぽい

イテレーターの自前実装

  • prociterator に変えればイテレーターが書ける

  • 値を返すときは yield

iterator range(a, b: int) : int =
  for i in a..b
    yield i
  • イテレーターでは再帰呼び出しが禁止される (実装上の都合なので将来的にはいい感じにしたいとかなんとか)

  • イテレーターと関数の名前空間は別になっているので、名前被りは ok

基本型

bool

  • true または false

  • short-circuit な and, or 演算子と、 not, xor, ==, != 演算子が使える

  • なぜか順序系のオペレータも定義されてる <, <=, >, >= (sort とかで便利って感じなのかな)

char

  • シングルクォートで囲んで表現

  • C 言語同様1バイトなので Unicode とかは無理

  • ただし int8 とはちゃんと区別されるので、 ord, char 関数で明示的に行き来する

  • int8 ではないので四則演算はなし、大小比較だけができる

string

  • 「string は mutable」って書いてあるけど let だと変更できなかったので、あたかも新しい値が作られて再代入されたかのように振る舞ってそう
    • 他のコレクションっぽい基本型も同様 (試した)。さらにネストされてる場合はディープコピー
    • たぶん ref (後述) とか使って参照を増やしたりしない限り基本 immutable。いい感じ
var hoge = "hoge"
hoge[2] = 'i'

echo hoge # hige
  • len メソッドで長さが取れる

  • & オペレータで concatenate、 add オペレータで append

  • [] オペレータで各バイト (unicode でない) にランダムアクセスできる

  • 辞書順で順序が定義されている

int

  • 符号付き整数

  • 桁数が足りなくなると最大 64bit までよしなにキャストされる

  • 後ろにビット数をつけると明示もできる (int8, ..., int64)

    • リテラルでビット数を明示したい場合は 10'i8 のように書く
  • 四則演算と順序が定義されている。割り算は / ではなく mod, div

  • ビット演算もあるが、unsigned かのように振る舞うので算術シフトが欲しい場合は div 2 する必要がある (!!)

    • and, or, xor, not, shl, shr
  • リテラルの途中のアンダーバーは無視される (1_000_000 のように書いても良い (!!))

  • リテラル先頭の 0x, 0b, 0o でベースを指定できる。 012 とかは普通に十進扱い

これ地味にトラップなので嬉しい。

uint

  • int に似ているが、 0 より小さくならない (アンダーフローもしない)

  • 上限に到達した場合も同様にオーバーフローしない

  • たんに負にならないことを型として明示したいだけなら、あとで出てくる Natural を使うべき

float

  • IEEE754 浮動小数点数

  • int と同様にビット数の指定ができる (float64, 0.5'f64 など)

  • 四則演算と順序が定義されている。割り算は /

  • toInt, toFloat で整数と行き来できる

ordinal types (順序型?)

  • int, char, bool と、このあと出てくる enum ..., subrange には以下のオペレーションを共通で使えて、これらを総称して ordinal type と呼ぶ
    • ord, inc, dec, succ, pred

キャスト

  • 型名を関数 (メソッド) のように使ってキャストできる
let x = int(2.5)
let y = 'a'.int8

文字列変換

  • たいてい $ メソッドが定義されていて、いい感じの文字列に変換できる

  • repr メソッドでは内部表現を得られる

複雑な型

コレクションっぽい型もあるけど、明示的に new したもの (後述) の参照以外は value type なので再代入でディープコピーされる

型定義

  • type 文で型の別名を定義できる
type
  longlong = int64
  ulonglong = uint64 # 複数もいける

enum; <name>, <name>, ...

  • C の enum 同様、0-origin で勝手に番号が振られる
type
  Direction = enum
    north, east, south, west

var x: Direction = south
  • 名前がかぶる場合は . で型を明示できる
var x = Direction.south
  • $ は自動でいい感じに定義される

range[<from>..<to>] (subrange)

  • ordinal type の一部だけを切り出した型
var hoge : range[1..3] = 3
  • たとえば組み込みの range Naturalrange[0..high(int)] (high はその ordinal type の最大値) として定義されているので、正の数であることを明示したいときはこれを使うと便利

set[<ordinal type>]

  • 高速な代わりに ordinal type しか格納できない set

    • もっと複雑なものを格納したい場合は標準ライブラリにある HashSet などを使う
  • bit vector で実装されているので 2^16 以上の値は突っ込めない

この辺の割切り感も「らし」くていいなあ。

  • リテラルは {}、中で range らしきものを書くといい感じに解釈される
var x: set[char] = { 'a'..'z', '0'..'9' }
  • 四則演算 (和集合、共通部分、差集合 etc) が定義されている
var x: set[char] = { 'a'..'z' } + { '0'..'9' } - { 'b' }
  • サブセット関係で順序も定義されている。おしゃれ

  • 特定の要素が入っているかは in, notin オペレータか contains メソッドで調べられる

echo ('a' in x)
  • 要素の追加削除は incl, excl メソッド

  • 要素数は len ではなく card。おしゃれ

array[<size>, <type>]

  • 固定長の配列
var x: array[2, int] = [1, 2]
  • array[<from>..<to>, <type>] とするとインデックスの範囲を変えられる (!!)
var x: array[2..4, int] = [0, 1, 2]
echo x[2] # 0 が表示される

これ地味に便利だなー

  • len で要素数、 low で一番小さい index, high で一番大きい index (inclusive)

  • 要素のセットは直感的な構文でできる

x[2] = 4
  • 二次元配列は配列の配列として定義する
var x : array[2, array[2, int]]

なんとなく型検査のアルゴリズム(というか equality の定義)を眺めていたら indexType, baseType という文字列が見えたんだけど、もしかして配列の大きさとして書く array[1..3, int]1..3 とかは型 (subrange) で、同様に型のレイヤーでは 20..2 の subrange を表すとかそんな感じになってるのかしら(未検証)。だとしたら [int, int] とか書いたらどうなるのか気になる。面白そうな言語だしちゃんとマニュアルも読みたいな。

seq[<type>]

  • 可変要素数の配列

  • heap にアロケートされる (実行効率に気をつけてねという意味だと思う) が、 value type なので再代入すると deep copy される

  • @[] で簡単に作れる

  • arr と同様に len, low, high などが定義されている

openArray[<type>]

  • 不定長の配列

  • コンパイル時に長さを確定できない配列を受け取るのに使う、実体は固定長配列なので動的に伸ばしたり縮めたりはできない

  • 引数の受け渡しが完了すれば seq と同様のメソッドが使える

  • ネストは実装上の都合でできない

この辺 C が見え隠れしてて好き…

varargs[<type>]

  • 不定長の配列

  • 可変長引数の関数を書くのに使う

  • 引数の受け渡しが完了すれば seq と同様のメソッドが使える

  • 引数がミックスされていた場合は暗黙に変換される

proc hoge (foo: int, bar: varargs[string]) : string =
  ...

echo hoge(1, 2, "hoge") # 2 は string に変換される

discard とか安全性もちらほら気にしているように見えるのに、ここはエラーにしないのはなんか理由があるのかしら…

HSlice[<type>, <type>]

  • 二項演算 .. によって作られる「範囲」を表すオブジェクト

    • case のパターンでも似た構文は出てきたが、こちらはれっきとしたオブジェクト
  • string などのシーケンス型は [] 演算子にスライスも受け取れるようになっているので、 substring を簡単に取得したり差し替えたり (!!) できる

var hoge = "hogehoge"
hoge[2..3] = "gi"

echo hoge # hogihoge

イテレーターでも同じような構文が使えたけど、これはイテレータと関数で名前空間が違うことをうまいこと使ってるのかな。よくできてるな…

  • イテレータと同様 ..< 構文も使えるのに加えて、後ろから数える ^n も使える
hoge[1..^1] # 頭1文字とケツ1文字を捨てる
hoge[^2..^0] # 後ろ3文字を取る

特に後者ができるのすごい、構文どうなってるの…。これが許されるなら hoge[^1] とかも書けそうに見えてしまうけどそれはできないっぽい。若干トラップ

object; <name>: <type>; ...

  • 基本的なレコード型

  • 型名に * を付与するとその型自体が、フィールド名に * を付与するとそのフィールドが外のモジュール (ファイル) から見えるようになる

type
  Foo* = object
    hoge* : int
    fuga : int # こっちは private なフィールド
  • リテラルは Constructor(<...args>) 形式
var hoge : Foo = Foo(hoge: 1, fuga: 2)
  • 相互再帰的な型も単一の type 文で宣言できる
type
  Hoge = ref object # 型自体を ref にしてもよいし、
    foo : ref Fuga # フィールドを ref にしてもよい
  Fuga = object
    foo : Hoge
  • case で union っぽいのも作れる
type
  ObjType = enum
    Hoge, Fuga
  Obj = object
    case type: ObjType # type は種類を表すフィールドになる
    of Hoge:
      hogeVal: int # type が Hoge だった場合のフィールド
    of Fuga:
      fugaVal1: int
      fugaVal2: string

var hoge : Obj = Obj(type: Hoge, hogeVal: 1)
var fuga : Obj = Obj(type: Fuga, fugaVal1: 2, fugaVal2: "fuga!")
  • <fieldname>= で setter を定義できる
type
  Obj = object
    foo = 1 # このフィールドは private

proc bar* (obj: Obj) : int =
  $obj.foo & "!!"

proc `bar=`* (obj: Obj, val: int) =
  obj.foo = val - 1

var hoge : Obj
new(hoge)

hoge.bar = 1 # 代入の構文を使うと setter が呼ばれる
echo hoge.bar # "0!!"
  • []= でインデックス記法の setter も定義できる
proc `[]=`* (obj: Obj, index: int, val: int) =
  if index == 0:
    obj.foo = val - 1

hoge[0] = 100
echo hoge # "99!!"

hoge[1] = 200
echo hoge # "99!!"

tuple; <name>: <type>; ...

  • object と同じことができるが、いくつか追加の特徴がある

  • 型名をつけずに使うことができる

var hoge : tuple[foo: int, bar: int] = (foo: 1, bar: 2)

型の equality は構造によって決まる。フォーマットが同じなら同じ型扱い

  • フィールド名もつけずに使うことができる
var hoge : (int, int) = (1, 2)
  • 解体は分割代入でできる
var (hoge, fuga) = (1, 2)
  • tuple.fieldNametuple[1] でも内容にアクセスできるが、インデックスは定数でないとダメ

ref <type>

  • いわゆるポインタ、 new(<var>) を使って明示的にヒープにアロケートしたオブジェクト (traced object) に対して使う
    • traced object は GC の管轄に入る
var hoge : (int, int)
new(hoge)

var hogeRef : ref (int, int) = ref hoge
  • <ref>[] で deref できる
    • <ref>[][1]<ref>[1] と書ける (暗黙に deref される)
echo hogeRef[]

ptr <type>

  • ref とだいたい同じだが、 alloc, dealloc で自力でメモリ管理する (untraced object)

  • GC の管轄外になるので実行時エラーの原因になりうる、基本的には使わない

低レイヤの操作も言語レベルで許されてる感じ好き

proc (<name>: <type>, ...) : <type>

  • 関数の引数や変数に proc を渡したいときに使う型

  • 第一級関数っぽいけど実は関数ポインタなので nil も渡せる

  • 呼び出し方を pragma で選べる

    • proc (foo: int) {.inline.} でインライン化、など
    • 呼び出し方は型レベルで区別されるので、呼び出し方の合わない関数は渡せない

マニュアルを見ると「呼び出し方」も相当いろんな種類がビルトインされてて最適化 / 低レイヤへの気概を感じる…。

  • proc 内で定義した proc や、 proc type のオブジェクト (関数ポインタ) は呼び出し方が暗黙に closure になる
    • 裸の procclosure ではないので厳密には区別されるが、 closure の期待されているところに裸の proc を投げるのは例外的に ok (なので普段はあまり気にする必要ない、グローバルな関数以外は環境を持ち回るんだなあくらいの認識で ok)

distinct <type>

  • 元の <type> と同じデータ表現を持ちつつ、互いに subtype の関係にはない型
type
  Hoge = distinct int

var
  hoge : Hoge = 1 # int と同じデータを持てる
  fuga : Hoge = 2

echo hoge + fuga # int の subtype ではないので `+` が未定義 → これはエラー

一瞬謎の機能に見えるけど、マニュアルを読みに行くと SQLdistinct string で定義するユースケースとかが載ってる。 SQL にサニタイズを強制すれば SQLi 対策になる。生の文字列連結とか危険なオペレータが全部未定義になるので安全

ジェネリクス

  • 型、関数などにはジェネリクスをつけられる
type
  Tree*[T] = ref object
    l, r : Tree[T]
    val: T

proc newNode*[T] (val: T, l: Tree[T], r: Tree[T]) : Tree[T] =
  new(result) # result 変数もヒープにアロケートできる
  result.val = val
  result.l = l
  result.r = r

モジュールシステム

  • モジュールはファイル

  • グローバル変数やメソッドも名前の後ろに * をつければ export できる

  • import は import <Module>, <Module>,... でできる

import hoge, fuga
import piyo except dosukoi # dosukoi 関数はいらない
from piyo import dosukoi # dosukoi 関数だけ欲しい
import hoge as h # 別名
  • C のような include <file>, ... もできる。多分 private なものにも全部アクセスできる。単純にファイルだけ分けたいときに使うっぽい

  • export されているシンボルが名前被りした場合は ModuleName.symbol で明示できる (メソッドで型が違う場合などはよしなにディスパッチされるので大丈夫)

OOP (継承)

前書きに「OOP の強力なコンセプトも使えるけど nim の OOP はミニマルだよ。 OOP は便利だけどいつも正解とは限らないよ、 inheritance より composition の方が良い場合も多いよ」とある。好き

  • object of <baseClass> で継承システムを有効にできる

  • ルートは RootObj

type
  Person = ref object of RootObj
    name* : string
    age : int
  Student = ref object of Person
    id : int

「nim では継承を使うことは完全に任意だが、もし実行時の型情報を使って継承関係を作りたいなら…」という表現が、これは nim のデフォルトのモデリング手法というわけではないんだぜ感をめちゃくちゃ出してて好き

{.inheritable.} プラグマ を使えば RootObj を継承することなく継承システムを有効にすることもできる

  • 型名でダウンキャストできる
proc getID (x: Person) : int =
  Student(x).id

ダウンキャスト不可なオブジェクトだった場合は実行時エラー

  • method で動的にディスパッチする関数が書ける
type
  Expr = ref object of RootObj
  Literal = ref object of Expr
    val: int
  Add = ref object of Expr
    l: Expr
    r: Expr

method eval (e: Expr) : int =
  quit "OVERRIDE ME!"

method eval (e: Literal) : int =
  e.val

method eval (e: Add) : int =
  eval(e.l) + eval(e.r)

evalExpr ならなんでも渡せるが、渡ってきたものの種類によって動的に振る舞いが変わる。 (proc も多重ディスパッチはできるが、型によってどの関数が呼ばれるかは静的に決まるのが違い)

コンパイル時に --multimethods:on をつける必要がある

例外

  • HogehogeError 系の型は例外オブジェクトを表す

  • どれも Exception を継承していて、エラーの詳細が入っている msg フィールドを持っている

  • raise で発射できる

raise newException(HogehogeError, "Oops!")

exception は必要なライフタイムが不明なので (newException で) ヒープにアロケートする必要がある。

  • try, expect, finally で例外をキャッチできる
try:
  let a = readLine(file)
except IOError:
  echo "IO Error!"
except: # 残りの例外を全部ハンドル
  echo "Unexpected error!"
  raise # 引数なしの raise は直近の例外を re-raise
finally:
  close(file) # 必ず最後に実行される

キャッチされない例外があったらそのまま親に渡っていき、誰もキャッチしなければ (finally だけ実行して) 死ぬ。

  • {.raises: [...].} プラグマを書くとそこにリストされていない例外が飛ぶ可能性がないか静的に検査してくれる

すごい。 Effect System というのの一部として実現されているらしく、実際にはもっと色々できるっぽい

テンプレート

  • 単純なマクロはテンプレートとして書ける

  • 型を指定するとマッチした場合だけ展開できるっぽい。 untyped なら何も考えずにいつも展開

template `!=` (a, b : untyped) : untyped =
  not (a == b)

実際 != はこのように定義されている (>, >=, in, notin, isnot なども同様)。関数でもいいように見えるが、こうしておくと == をオーバーロードするだけで != が適切に動くようになって便利。たしかに

  • const とテンプレートを組み合わせる例
const
  debug = true

template log (msg: string) =
  if debug: echo(msg)

var x = 3
log("x is " & $x)

コンパイル時に debug の値が分かっていれば、関数呼び出しまるごと消せるので引数の評価 (&, $ の処理) 分だけ実行効率を稼げる。賢いなあ

  • ブロックが書かれた場合は最終引数に渡る
template withFile (v: untyped, filename: string, mode: FileMode, body: untyped) =
  let fn = filename # filename を複数回使うのでここで eval しておく
  var v: File
  if open(v, fn, mode):
    try:
      body
    finally:
      close(v)
  else:
    quit("cannot open file " & fn)

withFile(file, "someText.txt", fmWrite):
  file.writeLine("hogehoge")

これって v はちゃんと body でキャプチャされつつ、 fn はハイジニック (名前被ってもよしなに gensym される) になってるのかな。きっとそうなんだろうな、すごいなあ…


!= なんかがテンプレートで定義されているの、一瞬賢いなあと思ったけど、しばらく使っているうちに、「これ普通に型レベルでインターフェース決まってた方が嬉しいわ (cf. Haskell の Ord 型クラス)」という感想になってきた。たとえば順序系の比較演算 <, <=, >, >= なんかもテンプレートで定義されていて、確か <= かな?だけ実装すれば残りは実装しなくても使えるとかだった気がするけど、そんな仕様覚えてられない…w

マクロ

  • 構文木オブジェクトを直接ゴリゴリする lisp 的なマクロも書ける
import macros

macro myAssert(arg: untyped): untyped =
  arg.expectKind nnkInfix # 中置記法以外がきたらエラーにする
  arg.expectLen 3 # 演算子、オペランド x2 の3要素が揃っていなかったらエラーにする
  let op  = newLit(" " & arg[0].repr & " ") # 演算子ノードから文字列リテラルを作る
  result = quote do: # lisp の quasi-quote みたいな感じ、構文木オブジェクトが生成できる
    if not `arg`:
      raise newException(AssertionError,$`arg[1]` & `op` & $`arg[2]`)

この定義のもとで

myAssert(a != b)

if not (a != b):
  raise newException(AssertionError, $a & " != " & $b)

に展開される。カッコが勝手に補われているあたり構文木を直接いじっているんだなあいう感じがある

開幕の

arg.expectHogehoge

は良い感じのコンパイルエラーを生成するために必要。検査に通ればマクロの展開にコケることはない状態が好ましい。

  • 引数を static[<type>] にすると式自体ではなく静的に計算された具体値が渡ってくるっぽい
  • 引数を typed にすると、任意の型を受け取りつつ実際に型推論が判定した型の情報も見えるっぽい
36
24
2

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
36
24