I am working on a macro package that will allow a user to declare an "attribute" in module A and then use it from module B. An attribute will have an identifier, a fully qualified name, and a type. The fully qualified name will be constructed from the module name and the identifier. This is inspired by Clojure's philosophy of defining namespaced keys that always represent the same value data type and semantics. For example, the "postalCode" attribute defined in the "mailingAddress" namespace would always represent a postal code in the same way, even if it sometimes occurs in a "contact" object and sometimes in a "business" object. I'd like to just export one thing from module A with the attribute name as its identifier, so that importing the attribute from module B will work as the programmer expects.
It is interestingly tricky in Nim to stash a type during compilation (such as in an attribute declaration macro) and then use that type in a subsequent declaration (such as for a tuple field). The following code does that, but it uses macros with static[var Attribute] parameters -- is that even intended to work!? -- and more broadly I wonder whether it can be simplified. Ideas?
As temporary simplifications while exploring the approach, this code
import macros, tables
type Attribute = tuple[name: string, typeAst: NimNode]
macro declareAttribute(name: untyped): untyped =
expectKind(name, nnkIdent)
nnkVarSection.newTree(
nnkIdentDefs.newTree(
nnkPragmaExpr.newTree(
newIdentNode($name),
nnkPragma.newTree(
newIdentNode("compileTime")
)
),
newIdentNode("Attribute"),
newEmptyNode()
)
)
macro defineAttribute(attr: static[var Attribute],
name, typ: untyped): untyped =
attr = (name: $name, typeAst: typ)
newEmptyNode()
template attribute(name, typ: untyped): untyped =
declareAttribute(name)
defineAttribute(name, name, typ)
macro data(tupleTypeName: untyped,
attr: static[var Attribute]): typed =
let nameAst = newIdentNode(attr.name)
let typeAst = attr.typeAst
quote:
type `tupleTypeName` = tuple[`nameAst`: `typeAst`]
attribute(firstName, string)
data(Person, firstName)
let p: Person = (firstName: "Sybil")
echo p.firstName
I am not sure about the rationale for this design. Why don't you just export
type PostalCode* = distinct string
and then use this type elsewhere? In this way, you can leverage Nim type system.
I am not really familiar with Clojure, but it looks like this design tries to replace what a type system would do for free. Can you share some more examples and motivation to understand what you are getting at?
@andrea, It's a broad topic. I went looking for a concise existing explanation, but couldn't find it. If you are interested enough to wade through some detail, this podcast transcript of an interview with Clojure's creator, Rich Hickey, is good. Here's my shot at excerpting enough bullet points from that talk to capture the general idea. At the end, I'll try to tie this back to how you asked the question.
(Quoting Rich Hickey.)
(Me speaking again.)
Going back to how you asked the question, I'm trying to define Attribute s that have both names and types, and that can be recombined freely into aggregate types (in Nim, tuples or objects). Although you can't see it yet in the code sample, my aim is to support both run-time and compile-time combinations of attributes. I want to verify at compile time as much as the programmer chooses to nail down at compile time, but allow the programmer to choose to do some things dynamically at run time.
Here's a simple example for reading a row from a database, doing some computation, and using the result in a REST reply: