皆さん初めまして。
アドベントカレンダー初参加です。ていうか、Qiita初投稿であります。技術も拙い素人学生ですが、以後お見知り置きを...
さて、今回はタイトルにもありますがnimのpragmaについて書こうと思います。nim固有の機能なのに出回っている情報が致命的に少ないので。
pragmaとは
pragmaはそもそもコンパイラに補足的に情報を与えるもので、C系の言語にある機能です。と言ってもnimの場合これらとはかなり様相が違っていて、C系のpragmaがコンパイラ毎に独自に存在している方言的な要素を持っているのに対し、nimはコンパイラ(トランスパイラ)が公式ののみなので実質言語の標準機能となります。
ユーザー定義も出来ます。強い。
import macros
macro tpub*(x: untyped): untyped =
## Marks a proc with an export asterisk when ``-d:testing`` is defined.
expectKind(x, RoutineNodes)
when defined(testing):
let n = name(x)
x.name = newTree(nnkPostfix, ident"*", n)
result = x
proc main {.tpub.} =
echo "test"
また、その機能もかなり広範に渡っており、キーワード並みの機能を持つpragmaや、コンパイルオプションを渡すもの、諸言語との連携を担当するものなどがあります。公式のマニュアルに記載されて居ないものもあり私も全てを理解しきれていない所がありますが、取り敢えず把握しているものを解説します。よくわかってない部分はほぼmanualの日本語訳です。
コア機能系
{.discardable.}
これを付けたprocは戻り値があっても代入せずに使える(discardをつける必要がない)ようになります。戻り値があっても、と言うところがvoidとの違いです。
{.experimental.}
nim標準仕様にまだ組み込まれていない試験段階の諸機能を使えるようにするpragmaです。特定の機能だけ呼び出すこともできます。後述するpush,popで範囲を制限できます。
# これらは{.experimanetal.}でまとめて有効化できます。
{.experimental: "parallel".}
{.experimental: "callOperator".}
{.experimental: "codeReordering".}
# 他にもあるが割愛
proc hoge():void = echo "hoge"
# 並列処理がなされる(parallel)
parallel:
for i in 0..9:
spawn hoge()
# pythonの__call__みたいな呼び出し方ができる(callOperator)
type Fuga = object
proc `()`(self: Fuga): void = echo "fuga"
let fugaobj = Fuga()
fugaobj() # -> fuga
Fuga()() # -> fuga
# 後方参照できる(codeReordering)
proc foo(): void = bar()
proc bar(): void = echo "bar"
foo() # -> bar
{.global.}
proc内でこれを変数につけるとグローバル変数となります。
proc xorshift(): int =
var
x {.global.} = 123456789
y {.global.} = 362436069
z {.global.} = 521288629
w {.global.} = 88675123
t {.global.}: int
t = x xor (x shl 11)
x = y
y = z
z = w
w = (w xor (w shr 19)) xor (t xor (t shr 8))
return w
echo xorshift() # -> 252977563114
echo xorshift() # -> 646616338854
{.pure.}
enumの要素の帰属先を指定しないとエラーを出すようにします。
type MetaJP = enum
hoge
fuga
echo hoge # -> hoge
type MetaEN = enum {.pure.}
foo
bar
echo MetaEN.foo # -> foo(fooだけで指定するとエラー)
{.shallow.}
オブジェクトの浅いコピーをするようにします。
{.bycopy.}
オブジェクトを引数にした時値渡しするようにします。
{.byref.}
オブジェクトを引数にした時参照渡しするようにします。
{.register.}
Cのregisterキーワードに相当します。
{.push.},{.pop.}
宣言以降、プログラム全体に影響を及ぼすpragmaの範囲を制限することができます。
{.push experimental: "parallel".}
parallel:
spawn echo "hoge"
spawn echo "fuga"
spawn echo "hego"
spawn echo "higu"
{.pop.}
# これよりあとではparallel statementは使えない
オブジェクト指向系
{.final.}
これをもつtypeは継承することを禁止します。
{.inheritable.}
これをもつtypeはRootObjを継承しなくても継承できるようになります。
{.base.}
method定義時にオーバーライドされる関数につけます。
type
Parent = object of RootObj
Child = object of Parent
method fn(self: Parent) {.base.} =
echo "I'm parent"
method fn(self: Child) =
echo "I'm child"
Parent().fn() # -> I'm parent
Child().fn() # -> I'm child
高速化系
{.inline.}
procをインライン化します。ちょっとtemplateとの使い分けに悩みますが、templateの方が副作用絡みだと直感に反する挙動を起こします?
template echoTemp(a: int) =
echo a
echo a
proc plus1(a: int): int =
echo "in: " & $a
result = a + 1
echoTemp(plus1(1))
# -> in: 1
# 2
# in: 1
# 2
proc echoInline(a: int) {.inline.} =
echo a
echo a
echoInline(plus1(1))
# -> in: 1
# 2
# 2
{.computedGoto.}
while true中のcaseをCソースでgotoに置き換える事で高速化するそうです。
type
MyEnum = enum
enumA, enumB, enumC, enumD, enumE
proc vm() =
var instructions: array[0..100, MyEnum]
instructions[2] = enumC
instructions[3] = enumD
instructions[4] = enumA
instructions[5] = enumD
instructions[6] = enumC
instructions[7] = enumA
instructions[8] = enumB
instructions[12] = enumE
var pc = 0
while true:
{.computedGoto.}
let instr = instructions[pc]
case instr
of enumA:
echo "yeah A"
of enumC, enumD:
echo "yeah CD"
of enumB:
echo "yeah B"
of enumE:
break
inc(pc)
vm()
{.unroll.}
コードサイズを犠牲にしてループ展開をします。※現在では無視されるそうです。
{.linearScanEnd.}
caseのパターンマッチにおいて、あるパターンが起こる割合が高い場合それらのパターンに特化するように最適化するpragmaのようです。(手元で適当にコード書いて試してみましたがあんまり速度は変わらず...使い方が間違っている?)
メタプログラミング系
{.immediate.}
引数の評価を遅延させます。macroで使われるものでしたが、引数をuntypedにすることが推奨になりこのpragmaは非推奨になっています。
{.inject.}
templateの呼び出し元に識別子をアクセスすることを許可します。
template injectTest() =
var x = 1
injectTest()
echo x # -> Error
template injectTest2() =
var x {.inject.} = 1
injectTest2()
echo x # -> 1
{.gensym.}
templateの呼び出し元に識別子をアクセスすることを許可しないようにします。type, var, let, constはデフォルトで**{.gensym.}が付き、それ以外は{.inject.}**です。
{.dirty.}
スコープを突き抜けるデンジャラスなpragmaです。詳しく言うと呼び出し先に展開されます。ご利用は計画的に。
template hoge(): int =
xx + 1
template fuga(x: int): int {.dirty.} =
var xx = x
hoge()
echo fuga(1) # -> 2
他言語連携系
{.importc.},{.header.}
Cの関数をnimの関数にバインドします。**{.varargs.}**を使うと可変長引数を取れます。
proc kbhit(): cint {.header: "<conio.h>", importc: "kbhit".}
while true:
let isKbhit = if kbhit() == 0: false else: true
echo isKbhit # -> true or false
break
proc printf(formatstr: cstring) {.header: "<stdio.h>", importc: "printf", varargs.}
printf("Hello, %s", "work") # -> Hello, work
{.exportc.},{.importCpp.},{.importObjc.},{dynlib}...
これらについてはこちらに詳しく解説なさっている記事があり、そちらを見ていただいたほうがよいと思われるゆえ割愛します。
Nimで他の言語との連携方法まとめ
{.cdecl.},{.stdcall.},{.fastcall.},{.safecall.}
恐らくDLL関数の呼び出し規約の方式を指定するものだと思われます。
{.emit.}
C/C++コードを直接埋め込めます。
{.emit: """
static int cvariable = 420;
""".}
{.push stackTrace:off.}
proc embedsC() =
var nimVar = 89
# access Nim symbols within an emit section outside of string literals:
{.emit: ["""fprintf(stdout, "%d\n", cvariable + (int)""", nimVar, ");"].}
{.pop.}
embedsC()
{.compile.}
C/C++コードをコンパイルしてリンクします。
{.link.}
オブジェクトファイルなどとのリンクができます。
when defined(gcc) and defined(windows):
when defined(x86):
{.link: "icon.res".}
else:
{.link: "icon.o".}
{.passC.}
Cコンパイラへパラメータを渡せます。
{.passL.}
リンカへパラメータを渡せるそうです。
明示系
{.noSideEffect.}
純粋関数であることを明示します。副作用を伴うとエラーになります。
nim0.19.0からfuncが純粋関数宣言のキーワードになったため、もう使う理由はないです。
{.closure.}
クロージャであることを明示します。
{.procvar.}
procの引数として渡せるprocであることを明示すると思われます。
現在はデフォルトの挙動であるらしいので、これも使用する必要はなさそうです。
{.compileTime.}
コンパイル時のみに使われる関数ないし変数であることを明示します。
{.noReturn.}
procが戻り値を返さないことを明示します。
追記:
コメントを頂きましたので追記します。
詳しく言うと、procが無限ループやquitなどを使用している「プログラムの終点」であるため"noReturn"であることを明示するようです。復帰することが出来ないという点で、同じく値を返さないvoidとは違います。
RustやTypeScriptで言うところのneverですね。
並列/平行系
{.thread.}
スレッド上で実行される関数であることを明示します。
{.async.}
これを付けると非同期関数になります。よって内部でawaitを使えます。
{.gcsafe.}
スレッドを使ってもGCが正しく作動することを明示します。
外の変数をいじる関数はこれを付けるとエラーになります。
エラーメッセージ系
{.error.}
エラーを出します。メッセージを付けることも出来ます。これもraiseとの使い分けに悩みますが、コンパイルが続行される場合があるようです。公式には「オーバーロードや型変換のために有効な操作がいくつかあることを除外するために特に便利です」とかいてあります。つまり、以下のような望ましくない使い方を弾くことができるということだと思われます。
{.fatal.}
致命的エラーを出します。**{.error.}**と違ってコンパイルが停止することを保証します。
{.warning.}
警告を出します。コンパイルは続行されます。
{.hint.}
警告というほどでもない時に使うようです。コンパイル進捗を出力するためにnim compilerでも使われていますね。
{.raises.}
raiseされるエラーを明示します。
proc raiseIOorOS() {.raises: [IOError, OSError].} =
if bo:
raise newException(OSError, "OSError")
raise newException(IOError, "IOError")
proc neverRaise() {.raises: [].} =
echo "never raise"
neverRaise()
raiseIOorOS(true)
{.effects.}
raiseできるエラーをhintで出力するようですが、使いどころがよくわかりません。
proc hogehoge(bo: bool) =
if bo:
raise newException(IOError, "IO")
{.effects.}
# -> Hint: ref IOError [User]
else:
raise newException(OSError, "OS")
proc hoge(bo: bool) =
if bo:
raise newException(IOError, "IO")
else:
raise newException(OSError, "OS")
{.effects.}
# -> Hint: ref IOError [User]
# -> Hint: ref OSError [User]
コンパイルオプション
{.check.}
コンパイル時チェックを行うか指定します。offにすると潜在的バグを通す可能性があります。
{.nilChecks.}
ぬるぽチェック
{.hints.}
offにするとhintが出なくなります。
{.optimization.}
最適化オプションを指定します。speedは時間計算量、sizeは空間計算量を最適化するようです。
{.deadCodeElim.}
到達不能コード、いわゆるデッドコードを除去します。
その他
{.pragma.}
pragmaを定義するpragmaです。
{.acyclic.}
循環するオブジェクトを生成する為に必要だそうです。
type Foo = ref object {.acyclic.}
data: Foo
id: int
{.volatile.}
volatileな変数を定義できるそうです。C言語などで、レジスタの読み書きをするときに使われるものです。
{.used.}
定義した関数を使用しなくても警告を出さないようにします。macro内部で使われる関数などで使うと便利です。
{.deprecated.}
関数が非推奨(廃止予定)であることを出力します。