Validating parameter format
newLisp is a loosely-typed language. However, if you are developing a library or module that other projects may mix into their own code, it is useful to give helpful errors when a function receives incorrect input. Especially if your documentation is lax (which we know it never is), throwing usable errors when a function is misused will make the lives of developers using your code that much easier.
Luckily, we can do this easily using newLisp using a simple macro. We will want to accept a predicate that will validate the value, which we will also need to accept, as well as the name of the calling function in order to produce a useful error message. However, we cannot accept a lambda predicate; we will be using the name of the predicate as part of the error.
Additionally, we may also find that an existing predicate will work for us, but only if negated- that is, we may want something that satisfies (not (string? foo))
. To allow that, we will accept predicates with a leading exclamation mark to signify that this should be reversed.
For the impatient, here is the finished product:
(define-macro (valid-format _pred _val _func , _conditional _err) (if (starts-with (string _pred) "!") (and (set '_conditional 'if) (set '_pred (sym (trim (string _pred) "!") MAIN)) (set '_err (format "unexpected %s value in %s" (trim (string _pred) "?") (string _func)))) (and (set '_conditional 'unless) (set '_err (format "%s value expected in %s" (trim (string _pred) "?") (string _func))))) ((eval _conditional) ((eval _pred) (eval _val)) (throw-error _err)))
Let’s go through this, piece by piece. Since macros do not evaluate their parameters, if a symbol is passed to a macro that has the same name as the macro’s parameter, the symbol’s meaning will be overridden in the local scope. To avoid this, we lead our symbols with an underscore. We also define a few local variables, conditional and err, for convenience.
In the next part of the body, we see if there is a leading exclamation point. We use this to decide whether to use if or unless in our final decision: (unless|if condition (throw-error ...))
. If we are negating the predicate, we also need to strip the exclamation point off of the symbol in order to evaluate it later. We then set an error message appropriate to the predicate using the name of the function.
The last expression evaluates the condition, applying the predicate to the value, and throwing the error message if the predicate fails. We can use the macro like this to validate function input:
;;; Using a regular predicate (define (foo bar) (valid-format string? bar foo)) (foo nil) ; => user error : string value expected in foo ;;; Using a negated predicate (define (foo bar) (valid-format !string? bar foo)) (foo "hello world"); => user error : unexpected string value in foo