NimでTypeScriptのPick<T, Keys>を実装する
こんにちは。今回は、TypeScriptのユーティリティ型の1つのPick<T, Keys>
を、Nimのマクロを使って実装します。
Pick<T, Keys>
サバイバルTypeScriptでは、Pick<T, Keys>
はこのように説明されています。
Pick<T, Keys>は、型TからKeysに指定したキーだけを含むオブジェクトの型を返すユーティリティ型です。
Pick<T, Keys>
はある型を元にしてデータ構造を定義したいときに便利です。
interface Todo {
title: string;
description: string;
completed: boolean;
}
type TodoPreview = Pick<Todo, "title" | "completed">;
const todo: TodoPreview = {
title: "Clean room",
completed: false,
};
TypeScriptでは型の1つとして振舞いますが、NimではTypeScriptほど高度な型パズルはできないため、マクロを用いて実装します。
実装
まずは引数から考えます。型を1つ、プロパティ名を無制限に受け付ける次のようなシグネチャにします。
macro Pick*(T: typed, keys: varargs[untyped]): type
引数は、1つ目に元となる型T
、それ以降に複数の識別子を受け付けるvarargs
のkeys
です。keys
ではプロパティ名だけ取得できればいいので、untyped
の識別子を取るようにしています。
次に、返り値について考えます。
type
Todo = ref object
title: string
description: string
completed: bool
type
TodoPreview = Pick(Todo, title, completed)
Pick
マクロはオリジナル同様新しい型を作れるようにしたいので、このような形で実装するのが理想です。
最後の行の右辺は次のように展開されます。
type
TodoPreview = ref object
title: string
completed: bool
このコードは、抽象構文木に直すとこうなります。
TypeSection
TypeDef
Ident "TodoPreview"
Empty
RefTy
ObjectTy
Empty
Empty
RecList
IdentDefs
Ident "title"
Ident "string"
Empty
IdentDefs
Ident "completed"
Ident "bool"
Empty
この構文義に従えば、RefTyから始まるノードを返せば実装できそうです。これらの情報を元に定義したPick
マクロがこちらになります。
proc decomposeIdentDefs*(identDefs: NimNode): seq[NimNode] {.compileTime.} =
for name in identDefs[0..^3]:
result.add newIdentDefs(
name = name,
kind = identDefs[^2],
default = identDefs[^1]
)
proc pickMatchingIdentDefs(
identDefs, keys: seq[NimNode]
): seq[NimNode] {.compileTime.} =
for k in keys:
var res: seq[NimNode]
for identDef in identDefs:
if k.eqIdent identDef[0].basename:
res.add identDef
if res.len == 0:
error fmt"Key {k} does not exist", k
result.add res
macro Pick*(T: typed, keys: varargs[untyped]): type =
# Tから型定義のノードを取得
let implNode = getImpl(T)
# T型のプロパティのidentDefsを格納するseq
var variables: seq[NimNode]
for identDefs in implNode[2][0][2][0..^1]:
variables.add decomposeIdentDefs(identDefs)
result = nnkRefTy.newTree(
nnkObjectTy.newTree(
newEmptyNode(),
newEmptyNode(),
newEmptyNode()
)
)
let recList = nnkRecList.newNimNode()
recList.add variables.pickMatchingIdentDefs(keys.toSeq())
result[0][0][2][0][2] = recList
受け取った型がref object
である前提で処理しています。
decomposeIdentDefs()
プロシージャは、そのままでは扱いにくい下記のようなidentDefsをすべて分解し同じ構造にして返します。
type ...
a, b: int
c, d: string
type ...
# すべて分解
a: int
b: int
c: string
d: string
pickMatchingIdentDefs()
は、名前の通り指定した名前のidentDefsを返し、存在しない名前の場合はエラーを吐くプロシージャです。
しかし、このPick
マクロを実行してみるとエラーを吐いてしまいます。実は、型定義の式の中での展開は一筋縄ではいきません。
一見するとPick
マクロはnnkRefTy
から始まるノードを返せばいいだけのように思えますが、型定義の式の中ではマクロの扱いが難しいためこの実装ではコンパイルが通りません。しかし、裏技のような解決策として、NimNodeKind
の1つであるnnkStmtListType
を使うことができます。
これはNim forum内のType macro returning a transformed type def + other definitionというスレッド内のあるリプライで言及されていますが、大まかに言えばnnkStmtListType
を返すマクロ/テンプレートなら型定義の式の右辺に書くことができるというものです。マイナーかつ裏技的な手法のため書き方も多少複雑になりますが、大体のロジックは完成しているのであとはこの手法に沿って返り値をつくればいいでしょう。
そして、完成したPick
マクロがこちらです。
macro Pick*(T: typed, keys: varargs[untyped]): type =
let implNode = getImpl(T)
var variables: seq[NimNode]
var internal: NimNode
result = nnkStmtListType.newNimNode()
for identDefs in implNode[2][0][2][0..^1]:
variables.add decomposeIdentDefs(identDefs)
internal = quote do:
type Internal = ref object
Internal
let recList = nnkRecList.newNimNode()
recList.add variables.pickMatchingIdentDefs(keys.toSeq())
internal[0][0][2][0][2] = recList
internal.copyChildrenTo result
最初のものと同様、受け取った型をref object
とみなして処理しています。
Internal
という型を定義していたり、最後にcopyChildrenTo()
を使っていたりするのはnnkStmtListType
固有の書き方なので、詳しくはリンク先をご覧ください。
これでようやくコンパイルが通り、きちんと実装できました!
type
User = ref object
name: string
age: int
address: string
type
Person = Pick(User, name, age)
proc new(_: type Person, name: string, age: int): Person =
result = Person(name: name, age: age)
let p {.used.} = Person.new("Steve", 100)
最後にobject
型にも対応し、エラーメッセージを加えたコード全文を置いておきます。
コード全文
import
std/macros,
std/sequtils,
std/strformat
proc decomposeIdentDefs*(identDefs: NimNode): seq[NimNode] {.compileTime.} =
for name in identDefs[0..^3]:
result.add newIdentDefs(
name = name,
kind = identDefs[^2],
default = identDefs[^1]
)
proc pickMatchingIdentDefs(
identDefs, keys: seq[NimNode]
): seq[NimNode] {.compileTime.} =
for k in keys:
var res: seq[NimNode]
for identDef in identDefs:
if k.eqIdent identDef[0].basename:
res.add identDef
if res.len == 0:
error fmt"Key {k} does not exist", k
result.add res
macro Pick*(T: typed, keys: varargs[untyped]): type =
let implNode = getImpl(T)
var variables: seq[NimNode]
var internal: NimNode
result = nnkStmtListType.newNimNode()
case implNode[2].kind
of nnkRefTy:
for identDefs in implNode[2][0][2][0..^1]:
variables.add decomposeIdentDefs(identDefs)
internal = quote do:
type Internal = ref object
Internal
let recList = nnkRecList.newNimNode()
recList.add variables.pickMatchingIdentDefs(keys.toSeq())
internal[0][0][2][0][2] = recList
of nnkObjectTy:
for identDefs in implNode[2][2][0..^1]:
variables.add decomposeIdentDefs(identDefs)
internal = quote do:
type Internal = object
Internal
let recList = nnkRecList.newNimNode()
recList.add variables.pickMatchingIdentDefs(keys.toSeq())
internal[0][0][2][2] = recList
else:
error "only ref object and object are acceptable", implNode[2]
internal.copyChildrenTo result
終わりに
ネットサーフィンでnnkStmtListType
を見かけたときにユーティリティ型のPick<T, Keys>
のことを思い出したので実装してみました。他のユーティリティ型もなんとかして実装できれば、Nimで擬似型パズルも可能なのでは…と夢見てしまいます。
Discussion