Keyword and default arguments can be used in Racket functions as shown on this page: https://docs.racket-lang.org/guide/lambda.html
(define greet
(lambda (#:hi [hi "Hello"] given #:last [surname "Smith"])
(string-append hi ", " given " " surname)))
> (greet "John")
"Hello, John Smith"
> (greet "Karl" #:last "Marx")
"Hello, Karl Marx"
> (greet "John" #:hi "Howdy")
"Howdy, John Smith"
> (greet "Karl" #:last "Marx" #:hi "Guten Tag")
"Guten Tag, Karl Marx"
Since Racket is said to be able to create new language definitions easily, is it possible to create a macro so that functions can be defined as follows:
(define (greet2 (hi "hello") (given "Joe") (surname "Smith"))
(string-append hi ", " given " " surname))
It should be possible to call the functions with arguments in any order as follows:
(greet2 (surname "Watchman") (hi "hi") (given "Robert") )
Just to clarify, following works:
(define (greet3 #:hi [hi "hello"] #:given [given "Joe"] #:surname [surname "Smith"])
(string-append hi ", " given " " surname))
(greet3 #:surname "Watchman" #:hi "hey" #:given "Robert" )
But I want following to work (parentheses may be () or [] or even {} ):
(define (greet4 [hi "hello"] [given "Joe"] [surname "Smith"])
(string-append hi ", " given " " surname))
(greet4 [surname "Watchman"] [hi "hey"] [given "Robert"])
Basically, I want to get rid of "#:surname" part (since it appears repetitive) to improve ease of typing.
How can such a macro be created? I attempted some code on the lines of:
(define-syntax-rule (myfn (arg1 val1) (arg2 val2) ...)
(myfn #:arg1 val1 #:arg2 val2 ...))
but it does not work.
Thanks for your comments / answers.
Edit:
I modified the code from answer by @AlexKnauth to use {} instead of [] and this also works well:
(require syntax/parse/define ; for define-simple-macro
(only-in racket [define old-define] [#%app old-#%app])
(for-syntax syntax/stx)) ; for stx-map
(begin-for-syntax
;; identifier->keyword : Identifer -> (Syntaxof Keyword)
(define (identifier->keyword id)
(datum->syntax id (string->keyword (symbol->string (syntax-e id))) id id))
;; for use in define
(define-syntax-class arg-spec
[pattern name:id
;; a sequence of one thing
#:with (norm ...) #'(name)]
[pattern {name:id default-val:expr}
#:when (equal? #\{ (syntax-property this-syntax 'paren-shape))
#:with name-kw (identifier->keyword #'name)
;; a sequence of two things
#:with (norm ...) #'(name-kw {name default-val})]))
(define-simple-macro (define (fn arg:arg-spec ...) body ...+)
(old-define (fn arg.norm ... ...) body ...))
(begin-for-syntax
;; for use in #%app
(define-syntax-class arg
[pattern arg:expr
#:when (not (equal? #\{ (syntax-property this-syntax 'paren-shape)))
;; a sequence of one thing
#:with (norm ...) #'(arg)]
[pattern {name:id arg:expr}
#:when (equal? #\{ (syntax-property this-syntax 'paren-shape))
#:with name-kw (identifier->keyword #'name)
;; a sequence of two things
#:with (norm ...) #'(name-kw arg)]))
(require (for-syntax (only-in racket [#%app app])))
(define-simple-macro (#%app fn arg:arg ...)
#:fail-when (app equal? #\{ (app syntax-property this-syntax 'paren-shape))
"function applications can't use `{`"
(old-#%app fn arg.norm ... ...))
Example usage:
> (define (greet5 hi {given "Joe"} {surname "Smith"})
(string-append hi ", " given " " surname))
> (greet5 "Hey" {surname "Watchman"} {given "Robert"})
"Hey, Robert Watchman"
And there is flexibility of order of arguments:
> (greet5 {surname "Watchman"} "Howya" {given "Robert"})
"Howya, Robert Watchman"
Now simple define statements are not working:
(define x 0)
define: bad syntax in: (define x 0)
Instead (old-define x 0)
works.
You can do this, but you'll need something slightly more complicated using
define-simple-macro
and anidentifier->keyword
helper function.You can define your own
define
form and your own#%app
to use for function applications, but to do that you need to expand into racket's old versions, so you need import renamed versions, using theonly-in
require form.You'll also need to map the
identifier->keyword
function over all the identifiers. A useful function for that isstx-map
fromsyntax/stx
. It's similar tomap
, but it works on syntax objects as well.To define a helper function for a macro to use to transform the syntax, you need to put it inside a
begin-for-syntax
This answer defines two versions of this: one that supports only named arguments, and one that supports both named and positional arguments. However, both of them will use the
identifier->keyword
helper function.Just named arguments
This new version of
define
takes thearg-name
s and transforms them into keywords using theidentifier->keyword
helper function, but since it needs to transform a syntax-list of them, it usesstx-map
.It then groups the keywords together with the
[arg-name default-val]
pairs to create sequences ofarg-kw [arg-name default-val]
. With concrete code, this will group the#:hi
with the[hi "hello"]
to create sequences of#:hi [hi "hello"]
which is what the old define form expects.This defines an
#%app
macro that will be inserted implicitly on all function applications.(f stuff ...)
will expand into(#%app f stuff ...)
, so(greet4 [hi "hey"])
will expand into(#%app greet4 [hi "hey"])
.This macro transforms
(#%app greet4 [hi "hey"])
into(old-#%app greet4 #:hi "hey")
.Using the new
define
form:These impliticly use the new
#%app
macro defined above:Omitting arguments makes it use the default:
And functions like
greet4
can still be used within higher-order functions:Both named and positional arguments
The macros above support only named arguments, so functions that use positional arguments instead can't be defined used with them. However, it's possible to support both positional arguments and named arguments in the same macro.
To do this, we would have to make square brackets
[
and]
"special" so thatdefine
and#%app
can tell between a named argument and an expression. To do that, we can use(syntax-property stx 'paren-shape)
, which will return the character#\[
ifstx
was written with square brackets.So to specify a positional argument in a
define
, you would just use a normal identifier, and to use a named argument, you would use square brackets. So an argument specification could be either one of those variants. You can express that with a syntax class.Since it is used by the macro to transform the syntax, it needs to be in a
begin-for-syntax
along withidentifier->keyword
:And then you can define
define
like this, usingarg:arg-spec
to specify thatarg
uses thearg-spec
syntax class.For a given
arg
,arg.norm ...
is either a sequence of one thing (for positional arguments) or a sequence of two things (for named arguments). Then sincearg
itself can appear any number of times,arg.norm ...
is under another ellipsis, so thatarg.norm
is under two ellipses.The
#%app
macro will use a similar syntax class, but it will be slightly more complicated because thearg
s can be arbitrary expressions, and it needs to make sure that normal expressions don't use square brackets.Again, an argument has two variants. The first variant needs to be an expression that doesn't use square brackets, and the second variant needs to be a name and an expression wrapped in square brackets.
And the
#%app
macro itself needs to make sure that it's not used with square brackets. It can do that with a#:fail-when
clause:Now
greet4
can be defined using named arguments, but it can also usestring-append
with positional arguments.Just like before, omitting arguments causes it to use the default.
What's different now is that positional arguments work,
And that square brackets
[
and]
can't be used for expression any more.Just like before,
greet4
can be used in higher-order functions likecompose
.Modifying it to support non-function definitions
The
define
macros above were specialized for function definitions, to keep it simple. However, you can support non-function definitions as well by usingdefine-syntax-parser
and specifying multiple cases.The
define-simple-macro
definition hereIs equivalent to using
define-syntax-parser
with one clause.So to support multiple clauses, you can write:
Then this will also support definitions like
(define x 0)
.