【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マクロの方はクラススコープの型定義機能がある