Hi!
I need a way to validate the value during attribute assignment for an object. Using macros as pragmas sounds like a good idea, and the syntax would be sane and clear:
{.this: self.}
type
Person* = ref object of RootObj
name: string
proc name*(self: Person): string = name
proc `name=`*(self: var Person, value: string) {.maxLength: 100.} =
name = value
proc newPerson*(name: string, age: int = 0): Person =
new result
result.name = name
The maxLength macro would look something like this:
macro maxLength*(limit: Natural, setterProc: untyped): untyped =
let lengthValidation = quote do: # This is the part I can't figure out.
if value > limit: # Obviously, this doesn't work since: value is undeclared,
raise newException(ValidationError, "Too long") # and even if I extract the variable from ``setterProc``, its value is unknown
setterProc.body.insert(0, lengthValidation)
result = setterProc
I can't figure out how to extract the runtime value of the value argument.
Maybe my approach is totally wrong. If you have a better suggestion, please adviseāI'm a novice and am very keen to learn from pros.
I come from the Python background, so I'm viewing macro in this example sort of like a decorator. I know they're very much different, but I don't know a better alternative to decorators or can't find a better programming pattern for the task. Again, any advice is appreciated.
Thanks for the quick response!
dirty template+getAst (the preferred way to construct ASTs these days).
Could you please point me to sample code where this technique is used? I'm pretty bad at using quote, but getAst is a totally unknown thing to me, and the docs don't say much.
Also, what do you mean by using a dirty template? Namely, what makes a template dirty?
Strangely, on my computer it did work. The compiler did not complain that value was not declared, but you were missing a len call to the parameter. By the way, you can also extract the exact identifier that is used in the setter, and therefore make shure that it is defined.
macro maxLength*(limit: Natural, setterProc: untyped): untyped =
let paramIdent = setterProc.params[2][0]
let lengthValidation = quote do: # This is the part I can't figure out.
if len(`paramIdent`) > `limit`: # Obviously, this doesn't work since: value is undeclared,
raise newException(RangeError, "Too long") # and even if I extract the variable from ``setterProc``, its value is unknown
setterProc.body.insert(0, lengthValidation)
result = setterProc
@Krux02 Thanks for the hint! I've already gone with the solution advised by @Araq though:
import macros
type
ValidationError* = object of Exception
template validateLength(limit: Natural) {.dirty.} =
if len(value) > limit:
raise newException(ValidationError, "The value is too long.")
macro maxLength*(limit: Natural, setterProc: untyped): untyped =
setterProc.body.insert(0, getAst(validateLength(limit)))
result = setterProc
I think it's more readable than using setterProc.params[2][0], because these indices don't really tell anything to the code reader. So it may be a good thing I couldn't make it work the quote way :-)
Still, I wonder if using dirty templates in conjunction with getAst is the easiest way to emulate decorator behavior. I have a sense there must be a better way (not that this way is bad).
UPD: On the second thought, I know tend to like the quote variant more. It's shorter and contains all the validation logic in a single macro. Maybe the setterProc.params[2][0] bit isn't that unreadable after all...
it's more readable than using setterProc.params[2][0]
You don't have to use setterProc.params[2][0] with your first approach (with quote do). Just value works - identifier will be resolved in the procedure being transformed, not in macro. Oppositely, to use the macro's symbol, you have to quote it with backticks. So what you had to change in your first example is to add backticks around limit (like in Krux02's example), using value as is (just with .len appended).
@LeuGim Wow! Well, this is just amazing, thanks a lot for the tip!
This is another case of Nim being awesome. I literally wrote the first sample as a throw-away-this-just-cannot-work-this-easily piece, and here it is, working perfectly well with just a pair of back ticks added.
@moigagoo Just be prepared, there might be some cases wher it is not that great after all.
@LeuGim Basically what you are saying was what I meant with "Strangely, on my computer it did work. The compiler did not complain that value was not declared, but you were missing a len call to the parameter". But maybe it did need different words to be understood.