チュートリアルを読み切ったので自分用メモ。競プロと相性良さそうだし使ってみようかなと思って勉強したら 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 # 一行読み込む
- 競プロ的には
strutils
のsplit
、sequtils
のmap
と併用することが多そう
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 は原則ナシ
- 配列などのインデックス参照 (
[]
) も特別なオペレータとして実装されているっぽい
イテレーターの自前実装
-
proc
をiterator
に変えればイテレーターが書ける -
値を返すときは
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
Natural
がrange[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) で、同様に型のレイヤーでは 2
は 0..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.fieldName
やtuple[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
になる- 裸の
proc
はclosure
ではないので厳密には区別されるが、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 ではないので `+` が未定義 → これはエラー
一瞬謎の機能に見えるけど、マニュアルを読みに行くと SQL
を distinct 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)
eval
は Expr
ならなんでも渡せるが、渡ってきたものの種類によって動的に振る舞いが変わる。 (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
にすると、任意の型を受け取りつつ実際に型推論が判定した型の情報も見えるっぽい