Edited at
N高Day 2

Nimでメタプロ(JSONシリアライズ)

More than 1 year has passed since last update.

N高 Advent Calendar 2日目の記事です。(N高ほとんど関係ない話になりますが)

Nimというプログラミング言語をご存じでしょうか?マイナーなプログラミング言語ですが、メタプログラミング機能がとても強力で最近お気に入りです。

公式: http://nim-lang.org/

この記事ではNimでのメタプログラミングで実際に簡単なプログラムを作っていこうと思います。


作るもの

メタプログラミングはコードの自動生成を行うものなので、今回は実装が簡単で自動生成という題材にぴったりなJSONシリアライズをやろうと思います。(JSONシリアライズは自分にとってのHello Worldみたいなものでもあるので)

具体的には、Nimで特定の型のJSONシリアライズ関数を自動で実装するマクロを書くということです。


実装

基本的な方針はオブジェクトのフィールド一覧を取得し、それを標準ライブラリjsonのオブジェクトでラップしていく関数を組み立てる感じです。

まずフィールド一覧を取得する関数を実装し、

type

Member = object
name*: string
typ*: string

# exportマーカーである`*`をASTから除去する関数
proc removePostfix*(ident: NimNode): NimNode =
if ident.kind == nnkPostfix:
return ident[1]
else:
return ident

# 型の実装からフィールド一覧を取得する関数
proc typeMembers*(node: NimNode): seq[Member] {.compileTime.} = # {.compileTime.}はコンパイル時のみに呼び出し可能にするプラグマ
var obj: NimNode
if node[2].kind == nnkRefTy:
obj = node[2][0]
elif node[2].kind == nnkPtrTy:
obj = node[2][0]
else:
obj = node[2]
result = newSeq[Member]()
for id in obj[2].children:
result.add(Member(name: id[0].removePostfix.repr, typ: id[1].repr))

そして値をjsonオブジェクトでラップする関数を実装します。

proc serializeJSON*(value: int): JsonNode =

return %* value # `%*`はjsonオブジェクトの組み立てDSL
proc serializeJSON*(value: string): JsonNode =
return %* value

今回はintとstringのみですが、似たような関数を型ごとに作れば他の型でも対応できます。また特定の型の場合に特殊なシリアライズをするということもできます。また、今回のマクロでJSONシリアライズ関数を自動実装したオブジェクトは、その他のオブジェクトのフィールドに使ってそのオブジェクトをシリアライズしても動作するようになっているはずです。

あとはオブジェクトのフィールドを取得し、フィールドごとにシリアライズ関数を呼び出す関数を組み立てて、

proc genSerializeJSON*(typ: NimNode): NimNode {.compileTime.} =

let members = typ.symbol.getImpl().typeMembers() # getImplで型や関数の実装の取得
var procstr = "proc serializeJSON*(this: $#): JsonNode =\n" % $typ # `%`は文字列の埋め込み
procstr &= " result = newJObject()\n"
for member in members:
procstr &= " result[\"$1\"] = this.$1.serializeJSON()\n" % member.name
return procstr.parseExpr()

マクロを実装すれば完成です。

macro implSerialize*(typ: typed): untyped =

return genSerializeJSON(typ)

これで以下のコードが動きます。

# 申し訳程度のN高要素

type
School* = object
since*: int
location*: string
name*: string

implSerialize(School)

var sch = School(since: 2016, location: "okinawa", name: "N high school")
echo sch.serializeJSON().pretty()

output:

{

"since": 2016,
"location": "okinawa",
"name": "N high school"
}


まとめ

Nimは標準ライブラリにjsonを扱えるライブラリがあるのでかなり楽をできました。

今回関数を組み立てる際に文字列ベースで組み立て、それをパースしましたが、NimではASTを直に組み立てることもでき、フォーラムなどを見るとそちらの方法をとっている人が多いので、そちらの方が推奨されると思います。(Nimはインデントでブロックを作る形式の構文なので文字列ベースの場合少々面倒というのもあります)

Nimは標準ライブラリにオブジェクトのシリアライズをしてくれる関数がある(marshalライブラリ)ので、今回作ったものを使うことは少ないかもしれませんが、マクロの練習にはなるかもしれません。

最後に全ソースコードを置いておきます。

Full source code:

import strutils, sequtils, json

import macros

type
Member* = object
name*: string
typ*: string

proc removePostfix*(ident: NimNode): NimNode =
if ident.kind == nnkPostfix:
return ident[1]
else:
return ident

proc typeMembers*(node: NimNode): seq[Member] {.compileTime.} =
var obj: NimNode
if node[2].kind == nnkRefTy:
obj = node[2][0]
elif node[2].kind == nnkPtrTy:
obj = node[2][0]
else:
obj = node[2]
result = newSeq[Member]()
for id in obj[2].children:
result.add(Member(name: id[0].removePostfix.repr, typ: id[1].repr))

proc serializeJSON*(value: int): JsonNode =
return %* value
proc serializeJSON*(value: string): JsonNode =
return %* value

proc genSerializeJSON*(typ: NimNode): NimNode {.compileTime.} =
let members = typ.symbol.getImpl().typeMembers()
var procstr = "proc serializeJSON*(this: $#): JsonNode =\n" % $typ
procstr &= " result = newJObject()\n"
for member in members:
procstr &= " result[\"$1\"] = this.$1.serializeJSON()\n" % member.name
return procstr.parseExpr()

macro implSerialize*(typ: typed): untyped =
return genSerializeJSON(typ)

type
School* = object
since*: int
location*: string
name*: string

implSerialize(School)

var sch = School(since: 2016, location: "okinawa", name: "N high school")
echo sch.serializeJSON().pretty()