Help us understand the problem. What is going on with this article?

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

More than 3 years have 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()
nnn-school
IT×グローバル社会を生き抜く“創造力”を身につけ、世界で活躍する人材を育成する。
https://nnn.ed.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした