⚜️

Nimのtuple interfaceで多重実装できるマクロをつくってみた

2023/08/28に公開

こんにちは、この記事では、複数のtuple interfaceを1つの型に実装するマクロを紹介します。
紹介するマクロは全て、oolibというライブラリに含まれていますので、内部の実装は割愛します。

tuple interfaceとは

NimにはJavaやKotlinのようなinterfaceが存在しませんが、tupleを使い擬似的に再現する手法があり、それをtuple interfaceと呼びます。

interfaces In Nim
NimでインターフェースとDIコンテナを使う

type
  IA = tuple
    a: int

  IB = tuple
    b: string

  ClassA = ref object
    a: int

  ClassA = ref object
    b: string

proc toInterface(self: ClassA): IA =
  result = (
    a: self.a
  )

proc toInterface(self: ClassB): IB =
  result = (
    b: self.b
  )

多重実装

しかし、この手法では複数のinterfaceを1つの型に実装することはできません。複数のtupleを合成して1つのtupleにしたようなものを書くことはできますが、冗長になります。

type
  ClassAB = ref object
    a: int
    b: string

type
  # 2つのtupleを合成する
  IAB = tuple
    a: int
    b: string

proc toInterface(self: ClassAB): IAB =
  result = (
    a: self.a,
    b: self.b
  )

そこで、tuple interfaceの記述やtoInterface、tupleの合成などのコードを自動生成するマクロをつくりました。

実装!

まずはtuple interfaceを定義するマクロをつくります。これは単に、受け取った名前とブロック内のnnkIdentDefsを整形してtupleの定義に放り込んでいるだけです。
また、interfaceというワード自体が予約語だったため、protocol[1]という名前にしました。

protocol IA:
  var a: int

protocol IB:
  var b: string

複数のtuple interfaceの実装

次に、複数のtupleを受け取り、実装した型を定義するマクロをつくります。このマクロは、クラス定義とそのクラスがtupleのメンバを満たしているかを検証する処理を同時に行うので、interfaceを持つ多くの言語に倣って、以下のようなclass-likeな文法をとるようにします。

class ClassA impl IA:
  ...

class ClassAB impl (IA, IB):
  ...

しかし、このマクロで受け取れるtupleの情報は名前のみで、メンバのシグネチャなどは一切参照できません。そこで、標準ライブラリの1つであるstd/macrocacheを使います。
このライブラリは、ざっくり言うとコンパイル時におけるマクロ間でのデータのやり取りを可能にする[2]ことができます。従って、protocolで受け取ったtupleの名前をキーにしてtupleの構文木を保存すれば、classで受け取ったtupleの名前からtupleの構文木にアクセスできます。

試行錯誤の末、こんな感じになりました。

protocol Readable:
  var text: string

protocol Writable:
  var text: string
  proc `text=`(value: string)

protocol Product:
  var price: int

class Textbook impl (Readable, Product):
  var
    text: string = ""
    price: int

class Notebook impl (Readable, Writable, Product):
  var text = ""
  var price: int
  proc `text=`(value: string) =
    self.text = value

classに渡された名前とメンバ、プロシージャから型定義のノードを組み立てつつ、それらを各tupleのノードと比較しています。実装していないプロシージャなどがある場合には該当のtupleのメンバにエラーが出るようになっています。また、class内のプロシージャには自動でその型のインスタンス自体を表すselfが挿入されています。

終わりに

今回紹介したprotocol, classは、oolibに収録されています。興味があったら覗いてみてください🙇‍♂️

脚注
  1. 元ネタは、Pythonのtypingモジュールで実装されているProtocolです。 ↩︎

  2. やり取りできるデータの型はNimNodeに限ります。 ↩︎

GitHubで編集を提案

Discussion