【Nim】クラス定義マクロを作ってみる
Nimにはクラスという概念がありません。しかし、オブジェクト指向プログラミング自体はサポートされており、object型を使えばクラスとほぼ同じことが可能になっています。ただし、object型の定義が微妙に面倒なことに加え、やはりクラスが欲しいということで、簡潔にクラス(object型)を定義できるマクロを作ってみました。
更新履歴
- 2021/06/27
- 最新のNim(v1.4.6)でビルドエラーになっていたのを修正しました
- GitHubにもコードを上げました(https://github.com/flatleon/NimClassMacro)
注意
- とりあえず作ってみたレベルなので、実際に使ってみると何か問題が出てくるかもしれません
- 対応Nimバージョン : 1.4.6
クラス定義マクロ
できあがったクラス定義マクロを使ったコードがこちらです。
# クラス定義 class Hoge: # クラススコープの型定義 (Hoge.Flagsなどでアクセス可能) type Flags = enum On, Off type MyInt = int # メンバ変数定義 var x: int var y: int # メンバ関数定義(第一引数としてself:Hogeが挿入される) proc test(a:int,y:int):bool = echo "Hoge.x=", self.x return false
今回作ったクラス定義マクロは以下のような特徴があります
- 通常の方法でobject型を定義するより簡素な記述方法で定義可能
- メンバ関数の定義が可能
- クラススコープの型定義が可能
それぞれ紹介します。
簡素な記述
まず、マクロを使わないで普通にobject型を定義する方法を紹介します。
type Hoge = ref object of RootObj x: int y: int
問題はref object of RootObjです。1つのobject型を定義するのにこれを毎回入力するのはかなり面倒です。いちおう、type hoge = objectという記述でもobject型を定義できるのですが、派生できない、代入時にコピーされてしまう、デストラクタが定義できない、などいろいろな挙動が変わってしまうので、Nimではref object of RootObjが基本的なobject型の定義方法だと思っています。
これをclassマクロを使えば以下のように簡潔に記述できます。
class Hoge:
var x: int
var y: int
基底クラスを指定したい場合は以下のように2通りの記述方法が利用できます。
class Hoge of Base: var x: int var y: int class Hoge(Base): var x: int var y: int
メンバ関数の定義が可能
Nimのobject型にはメンバ関数という概念はありません。そのかわりにメソッド構文というプロシージャをメンバ関数のように呼び出すことができる構文があります。この構文は第一引数.プロシージャ名(第二引数...)というような記述方法になります。なのでNimでメンバ関数相当のものを定義するには、第一引数でそのクラスのインスタンスを受け取るようにします。
proc test(x:Hoge) = discard # Hoge型.test()でアクセスできる -> つまり、Hoge型のメンバ関数として扱える
classマクロでは、第一引数にクラス型引数を自動で挿入することで楽にメンバ関数(相当のもの)を定義できるようにしています。また、classブロック内に記述することになるので、メンバ関数ということがよりわかりやすくなります。
class Hoge:
var x: int
var y: int
proc test() = discard
# マクロによって以下のように第一引数にself:Hogeが追加される
# proc test(self:Hoge) = discard
クラススコープの型定義が可能
C++ではクラス定義内でtypedefやenum定義をするとそのクラスに属するものとして扱われます。つまりクラススコープというものが存在します。Nimにはこのようなものが存在しません*1。ですが、templateを使うことで似たようなものを実現することができます。
type Hoge = ref object of RootObj x: int y: int template Flags(T:typedesc[Hoge]) : untyped = `T Flags` # Hoge.Flags を HogeFlagsへと変換するtemplate type HogeFlags = enum On, Off # Hoge.FlagsはHogeFlagsへと変換されるのでHogeFlagsという名前にしておく var f: Hoge.Flags # クラススコープのようなものを実現
アイデアとしては、メソッド記法とtemplateの識別子結合機能(Identifier construction)を使って、クラス名.型名というコードをクラス名型名に変換するというものです。これによってクラス名.型名という記述方法が可能になり、利用者側からみたらクラススコープとして扱うことができるようになります。型の定義側は、型名の頭にクラス名を付けることと、クラス名.型名というアクセスを可能にするためのtemplateを定義する必要があります。classマクロはこの作業を自動化します。
class Hoge:
x: int
y: int
type Flags = enum On, Off # クラススコープで型(enum)定義
# classマクロによりFlagsはHogeFlagsという型名になる
# また、Hoge.Flagsでのアクセスを可能にするためのtemplateが自動で挿入される
var f: Hoge.Flags # Hoge.Flagsでアクセス可能
classマクロによるコード展開例
冒頭のclassマクロを使ったコード例はclassマクロによって以下のように展開されます*2。
type Hoge = ref object of RootObj x: int y: int type HogeFlags = enum On, Off type HogeMyInt = int proc test(self:Hoge,a:int,y:int):bool = echo "Hoge.x=", self.x return false template Flags(T:typedesc[Hoge]) : untyped = `T Flags` template MyInt(T:typedesc[Hoge]) : untyped = `T MyInt`
classマクロ実装コード
とくにライセンスとかは設定しないので、ご自由にお使いください。ただし動作保証はしません。
{.experimental.}
import typetraits
import macros
import strutils
type TraverseOp = enum Continue, Break, SkipChild, SkipSibling, Finished
proc traverse(n: NimNode; action:proc(n:NimNode;parents:ref seq[NimNode]):TraverseOp; parents:ref seq[NimNode] = nil ):TraverseOp {.discardable.} =
var parents = parents
if parents == nil:
parents.new
parents.newseq( 0 )
parents.add( n )
defer:
discard parents.pop()
for it in n.children:
case action( it, parents )
of Continue: discard
of Break: return Break
of SkipSibling: return SkipSibling
of SkipChild: continue
else: assert false
case traverse( it, action, parents )
of Break: return Break
else: discard
return Finished
proc findNode(n:NimNode,kind:NimNodeKind):NimNode=
var ret: NimNode
n.traverse do (n:NimNode;parents:ref seq[NimNode]) -> TraverseOp:
if n.kind == kind:
ret = n
return Break
result = ret
proc convertToMemberProc( procDefNode:NimNode, className:string ) =
## プロシージャをメンバ関数化する -> 第一引数に指定の型のselfパラメータを追加するだけ
var formalParamsNode = procDefNode[3]
formalParamsNode.expectKind( nnkFormalParams )
var selfIdentNode = newIdentDefs( ident("self"), ident(className) )
formalParamsNode.insert( 1, selfIdentNode )
macro classproc*(className:untyped,stmtList:untyped):untyped=
var classNameStr = className.strVal
stmtList.traverse do (n:NimNode;parents:ref seq[NimNode]) -> TraverseOp:
case n.kind
of nnkProcDef, nnkMethodDef, nnkIteratorDef:
n.convertToMemberProc( classNameStr )
else: discard
result = stmtList
proc newClassDef(classNameIdent,baseNameIdent,classBody:NimNode):NimNode=
## クラス名ノード、基底クラス名ノード、フィールド定義ノードからクラス定義ノード(実際はtypeセクション)を作成する
# [1].とりあえずフィールド無しのobject型定義ノードを作成
# [2].classBodyを走査しidentDefsを見つけ次第、[1]内のRecListへコピーしていく
# また、ProcDefを見つけた場合は、第一引数にselfを追加してから、resultノードへProcDefを追加する
if classBody == nil: return
# [1]
var classStmt = quote:
type `classNameIdent` = ref object of `baseNameIdent`
result = newStmtList( classStmt )
# RecListノードを取得(なければ作る)
var objectTyNode = classStmt.findNode( nnkObjectTy )
if objectTyNode[2].kind == nnkEmpty:
objectTyNode.del( 2 )
objectTyNode.add( newNimNode( nnkRecList ) )
var recListNode = objectTyNode[2]
# [2]
var result2 = result
classBody.traverse do (n:NimNode;parents:ref seq[NimNode]) -> TraverseOp:
case n.kind
# プロシージャの引数にselfを追加
of nnkProcDef, nnkMethodDef, nnkIteratorDef:
var newNode = n.copyNimTree()
result2.add( newNode )
newNode.convertToMemberProc( classNameIdent.strVal )
return SkipChild
# 変数定義はフィールド定義へ追加する
of nnkIdentDefs:
recListNode.add( n )
return SkipChild
# 型定義は型名をクラス名+型名に変更する
of nnkTypeSection:
# TypeSection内の識別子にクラス名を挿入
n.traverse do (n:NimNode;parents:ref seq[NimNode]) -> TraverseOp:
case n.kind
of nnkTypeDef:
var parentNode = parents[^1]
if parentNode.kind == nnkTypeSection:
# クラス名.型名でアクセスできるようにするためのヘルパーtemplateを定義
var helperTemplateStr = "template $1(T:typedesc[$2]) : untyped = `T $1`".format( n[0].strVal, classNameIdent.strVal )
result2.add( parseStmt( helperTemplateStr ) )
# 「型名」を「型名+クラス名」に変更
n[0] = newIdentNode( classNameIdent.strVal & n[0].strVal )
return SkipChild
else:discard
result2.add( n.copyNimTree() ) # TypeSectionまるごとコピー
return SkipChild
else: discard
macro class*(className:untyped,classBody:untyped):untyped=
# クラス名と基底クラス名を取得
var classNameStr:string
var baseNameStr:string
case className.len()
of 0: # class a
classNameStr = className.strVal
of 2: # class a(b)
classNameStr = className[0].strVal
baseNameStr = className[1].strVal
of 3: # class a of b
classNameStr = className[1].strVal
baseNameStr = className[2].strVal
else: assert false
result = newClassDef( newIdentNode(classNameStr), if baseNameStr.len() > 0:newIdentNode(baseNameStr) else:newIdentNode("RootObj"),classBody)
#######################
# 使用例
#######################
class Hoge:
# クラススコープの型定義 (Hoge.Flagsなどでアクセス可能)
type Flags = enum On, Off
type MyInt = int
# メンバ変数定義
var x: int
var y: int
# メンバ関数定義(第一引数としてself:Hogeが挿入される)
proc test(a:int,y:int):bool =
echo "Hoge.x=", self.x
return false
# クラススコープで定義したenumを利用
var f: Hoge.Flags
echo f # On
# クラススコープで定義した型を利用
var myInt: Hoge.MyInt = 10
echo myInt # 10
# あとからメンバ関数を追加するためのマクロもあり
classproc Hoge:
proc test2(a:int,y:int):bool =
echo "Hoge.test2()"
return false
# クラス(object型)として普通に利用できる
var temp = Hoge(x:77)
echo "$1, $2".format( temp.x, temp.y )
discard temp.test(1,2)
discard temp.test2(1,2)
改善案
- コンストラクタを定義可能に
- メンバ変数の初期値指定
- staticメンバ変数
- staticメンバ関数
- メンバのアクセス制御(といってもNimはモジュール内外のアクセス制御しかないが…)
- Self型、Base型エイリアス
Nim by Example の OOPマクロとの比較
このようなクラス定義マクロは公式?の Nim by Example にも存在しています。今回作ったマクロも「Nim by Example」のOOPマクロを参考にしています*3。
「Nim by Example」のOOPマクロと今回作成したclassマクロの違いは以下とおりです。
- OOPマクロは基底クラスの指定が必須になっている
- classマクロの方は基底クラスの指定方法として
of以外にclass クラス名(基底クラス名):の形式にも対応している - classマクロの方はクラススコープの型定義機能がある
