When trying the js backend, I often need to write objects that get compiled to js objects.
In javascript, it is frequent to make functions that accept a single object whose keys are interpreted as named arguments. One can pass a subset of the valid properties to mean that the rest are to be left default.
In Nim, if I have an object, say
type Foo = object
a, b: cstring
I can initialize only part of its properties, like this
let foo = Foo(a: "hello")
This gets compiled to
var foo = {
a: "hello",
b: null
};
Notice that Nim always adds the b key, putting null as default. What I need is a way to obtain
var foo = {
a: "hello"
};
This is for two reasons:
How can one avoid to render missing properties in the js backend (short of writing an object type for any combination of keys, which is combinatorially infeasible)?
You might want to use procs as field getters/setters.
type Foo = ref object
proc `a=`(f: Foo, a: cstring) = {.emit: "`f`.a = `a`;".}
proc a(f: Foo) = {.emit: "`result` = `f`.a;".}
proc `b=`(f: Foo, b: cstring) = {.emit: "`f`.b = `b`;".}
proc b(f: Foo) = {.emit: "`result` = `f`.b;".}
let myObj = Foo.new()
myObj.a = "hello"
I don't like that @yglukhov... the object has no fields and types anymore. This limits for example nimsuggest, makes for bad documentation and general overview. Therefor it harms my editor and productivity. It may also not be a good solution here, because having many keys would mean a lot of boilerplate code.
It simply should not be possible normally. But then we are using Nim for a reason:
# nim js -d:release -d:nodejs --verbosity:0
type Foo = ref object
a: cstring
b: int
proc isUndefined*[T](x: T): bool {.importcpp: "((#)==undefined)".}
proc main() =
var o {.noinit.}: Foo
{.emit: "var `o` = {};".}
o.a = "test"
#[ this is Nim not javascript
if o.b == nil:
echo "o.b is not is nil ever"
]#
if o.b.isUndefined:
echo "but then we are Nim and Nim is King"
main()
I have some more stuff like a JSAssoc object in my Nim-Screeps project which may be of interest. We used something similar for our PHP backend to interface to PHPs associative Arrays. But that is "freeform" again. In Nim-Screeps I try to trick everything into fully typed code. Which pretty much works very well IMHO.
I also take suggestions of course. I merely hacked that together myself in the last weeks (with a little help and some small compiler fixes by @araq).
https://github.com/oderwat/nim-screeps/blob/master/src/jsext.nim
I added a "safe(int): int" proc to the above example (for convenience):
# nim js -d:release -d:nodejs --verbosity:0
type Foo = ref object
a: cstring
b: int
proc isUndefined*[T](x: T): bool {.importcpp: "((#)==undefined)".}
proc safe*(x: int): int {.importcpp: "((#)||0)".}
proc main() =
var o {.noinit.}: Foo
{.emit: "var `o` = {};".}
o.a = "test"
#[ this is Nim not javascript
if o.b == nil:
echo "o.b is not is nil"
]#
if o.b.isUndefined:
echo "but then we are Nim and Nim is King"
echo o.b.safe # 0
o.b = 3
echo o.b.safe # 3
main()
Fox context: I am trying to write a wrapper around React and there are holder objects for HTML attributes and CSS styles. An example of usage is here
proc renderComponent(s: TopLevel): auto =
section(
section(Attrs(className: "row", key: "search"),
section(Attrs(className: "col-md-4"),
React.createElement(search, ValueLink(
value: s.state.query,
handler: proc(q: string) = s.setState(Filter(query: q))
))
)
),
section(Attrs(className: "row", key: "list"),
React.createElement(items, ItemFilter(
countries: s.props.countries,
query: s.state.query
))
)
)
For the usability of the DSL, it is critical that one should be able to write the attributes inline.
I can think of a few ways to overcome this: one is to write myself a procedure that has a nil default for all fields and then initializes the object as OderWat suggest, but only for values that are not nil.
But this becomes rapidly very taxing, and I would rather find some more convenient alternative. I cannot use something like a typed map (the example in screeps), because the value types change with the key
BTW: I am more a fan of VueJS though... which is an underdog and pretty cool... a bit like Nim :)
Thank you, I know about VueJS, but I don't like it very much. I prefer to avoid abusing HTML to recreate a Turing complete language in it - creating HTML as a tree inside javascript makes much more sense to me. I also prefer the declarative approach of react instead of data binding. To each one its own, I guess :-)
About the macro: I have this attempt
macro attrs*(xs: varargs[untyped]): Attrs =
let a = !"a"
var body = quote do:
var `a` {.noinit.}: Attrs
{.emit: "`a` = {};" .}
for x in xs:
let
k = x[0]
v = x[1]
body.add(quote do:
`a`.`k` = `v`
)
body.add(quote do:
return `a`
)
result = quote do:
proc inner(): Attrs =
`body`
inner()
and I feel it should work.
But of course it doesn't. Namely, whenever I call it, say, like this
let x = attrs(key = "hi", placeholder = "foo")
I find that xs (the varargs) is just Bracket. The parameters seem not to be passed at all. How do macros with varargs[untyped] work? I am trying to pass a variable number of... well, whatever, as long it is a piece of an AST that I can decompose and use to extract information.
I have been able to make this work using untyped and wrapping all parameters inside a single node that is parsed as an object declaration.
The macro is mostly the same:
macro attrs*(xs: untyped): Attrs =
let a = !"a"
var body = quote do:
var `a` {.noinit.}: Attrs
{.emit: "`a` = {};" .}
for x in xs:
if x.kind == nnkExprColonExpr:
let
k = x[0]
v = x[1]
body.add(quote do:
`a`.`k` = `v`
)
body.add(quote do:
return `a`
)
result = quote do:
proc inner(): Attrs {.gensym.} =
`body`
inner()
Unfortunately the usage site becomes a little cumbersome. One has to call it like this:
attrs(only(key: "hi", placeholder: "foo"))
Apart from the only name (any other symbol would do, actually) it is exactly what I wanted.
Any suggestion for improvements?
The following used to work but is currently broken (devel). So maybe wait until Araq fixed that. It should be possible to parse the arguments without the cumbersome workaround.
import macros
macro showArgs(n: varargs[untyped]): untyped =
for x in n.children:
if x.kind == nnkExprEqExpr:
let field = x[0].repr
case x[1].kind
of nnkStrLit:
echo field, ": ", x[1].strVal
of nnkIntLit:
echo field, ": ", x[1].intVal
else:
echo "not supported"
result = parseStmt("discard")
showArgs(cologne = 4711, foo = "foo the string")
Output:
cologne: 4711
foo: foo the string
As I said above, the macro that I have posted above now works (I also post it below for reference). Still, I think that it would be useful to have it in the JS standard library, since it is a very common need when interacting with existing JS libraries.
The issue is that I have not been able to abstract it over the type. I have tried various ways of wrapping this in a template or macro that accepts Attrs as a typedesc parameter, but I have not been able to get anything meaningful.
Anyone wants to try to make it more generic?
macro attrs*(xs: varargs[untyped]): Attrs =
let a = !"a"
var body = quote do:
var `a` {.noinit.}: Attrs
{.emit: "`a` = {};" .}
for x in xs:
if x.kind == nnkExprEqExpr:
let
k = x[0]
v = x[1]
body.add(quote do:
`a`.`k` = `v`
)
else:
error("Expression `" & $x.toStrLit & "` not allowed in `attrs` macro")
body.add(quote do:
return `a`
)
result = quote do:
proc inner(): Attrs {.gensym.} =
`body`
inner()