define2
1 Define and lambda
no-value
no-value?
lambda
λ
define
2 Wrapper functions
define-wrapper
3 Compile-time error checking
4 Fail case
5 Struct2
struct2
5.1 Inheritance
5.2 Fenders and super
6 Acknowledgements
9.1

define2🔗ℹ

Laurent Orseau

1 Define and lambda🔗ℹ

 (require define2) package: define2

There may be incompatibility with code that uses the keywords #:! and #:?.

The define2 collection redefines lambda and define in a (almost entirely) backward compatible way to provide the following functionalities:
  • a shortcut definition for keyword arguments to avoid the ubiquitous #:some-arg some-arg repetition,

  • a pass-through mechanism for optional keyword arguments to propagate default values without having to know them.

Example: Mandatory #:! argument
> (define (make-fruits fruit #:! number)
    (make-list number fruit))
> (make-fruits 'apple #:number 4) ; Notice the keyword name

'(apple apple apple apple)

Example: Optional #:? argument
> (define (make-fruits2 fruit #:? [number 3])
    (make-list number fruit))
> (make-fruits2 'pear)

'(pear pear pear)

> (make-fruits2 'pear #:number 4)

'(pear pear pear pear)

Example: Pass-through #:? argument
; Let's write a function that uses `make-fruits2` without changing
; the default value for `number`—whatever value this is.
> (define (make-two-fruits fruit1 fruit2 #:? number)
    (list (make-fruits2 fruit1 #:number number)
          (make-fruits2 fruit2 #:number number)))
> (make-two-fruits 'apple 'banana)

'((apple apple apple) (banana banana banana))

> (make-two-fruits 'apple 'banana #:number 2)

'((apple apple) (banana banana))

The usual racket syntax #:keyword [id val] can also be used with pass-through arguments:
> (define (make-fruits3 fruit #:number [a-number 3])
    (make-list a-number fruit))
> (define (make-fruits4 fruit #:? number)
    (make-fruits3 fruit #:number number))
> (make-fruits4 'clementine)

'(clementine clementine clementine)

value

no-value : symbol?

procedure

(no-value? x)  boolean?

  x : any/c
no-value is an uninterned symbol representing the default value of pass-through arguments. no-value does not normally need to be used, but is provided for clarity and possibly for user enhancements.

syntax

(lambda args body ...+)

syntax

(λ args body ...+)

 
args = (pos-id ... [opt-id opt-expr] ... kw-arg ...)
  | (pos-id ... [opt-id opt-expr] ... kw-arg ... . rest-id)
  | rest-id
     
kw-arg = #:! id
  | #:? id
  | #:? [id expr]
  | keyword id
  | keyword [id expr]
Like lambda and λ from racket/base, but with support for #:! mandatory keyword arguments and #:? optional keyword arguments.

An argument of the form #:! name is equivalent to #:name name. An argument of the form #:? [name val] is equivalent to #:name [name val] but binds name to val only if name is no-value. An argument of the form #:? name is equivalent to #:name [name no-value].

This means in particular that (lambda (#:a the-a #:! a) ...) is a syntax error (duplicate argument keyword), as well as (lambda (#:a the-a #:! the-a) ...) (duplicate argument identifier).

syntax

(define id expr)

(define (head args) body ...+)
Like define from racket/base, but uses lambda from define2 instead. Also supports the curried form.

2 Wrapper functions🔗ℹ

 (require define2/define-wrapper) package: define2

Writing wrapper functions is already simplified with the new define thanks to pass-through optional arguments, but there can still be some verbosity left due to having to repeat the argument names. define-wrapper helps with this by passing the arguments to the wrapped function automatically.

syntax

(define-wrapper (fun [wrapped-fun arg ... maybe-rest]
                       keyword-arg ...)
  maybe-call-wrapped
  body ...)
 
maybe-call-wrapped = 
  | #:call-wrapped call-wrapped-id
arg ... maybe-rest and keyword-arg ... are arguments as for lambda, but keyword-arg ... are restricted to keyword arguments.

The resulting function fun takes as input all the arguments arg ... keyword-arg ... maybe-rest. Only the arguments arg ... maybe-rest are forwarded to the call to wrapped-fun. The function wrapped-fun must be defined elsewhere.

If call-wrapped-id is not provided then wrapped-fun is called in tail-position; otherwise it should be called as (call-wrapped-id) somewhere in body ..., and this calls wrapped-fun with the arguments arg ... maybe-rest.

More concretely (supposing that bar is already defined elsewhere),

(define-wrapper (foo (bar a #:? [b 'b])))

is equivalent to
(define (foo a #:? [b 'b])
  (bar a #:b b))
and
(define-wrapper (foo (bar a #:? [b 'b])
                     #:c c)
  (set! a (+ a c)))
is equivalent to
(define (foo a #:? [b 'b] #:c c)
  (set! a (+ a c))
  (bar a #:b b))
and
(define-wrapper (foo (bar a #:? [b 'b])
                     #:c c)
  #:call-wrapped bar-wrapped
  (set! a (+ a c))
  (define res (bar-wrapped))
  (displayln res)
  res)
is equivalent to
(define (foo a #:? [b 'b] #:c c)
  (set! a (+ a c))
  (define res (bar a #:b b))
  (displayln res)
  res)

Note: Be careful to not use pass-through arguments if the corresponding argument in wrapped-fun is not an optional keyword argument.

For example, this fails:
> (define-wrapper (my-sort (sort l <? #:? key)))
> (my-sort '(1 4 2) <)

sort: contract violation

  expected: (any/c . -> . any/c)

  given: 'no-value

But this is fine:
> (define-wrapper (my-sort2 (sort l <? #:? [key values])))
> (my-sort2 '(1 4 2) <)

'(1 2 4)

3 Compile-time error checking🔗ℹ

In standard Racket, when a function is defined with define, and is later called with the wrong number of arguments or with the wrong keywords, an error is signalled only at run time, when the function is called.

Instead, using the define form provided by define2 signals an error at compile-time.

Thus, such errors are caught much earlier than with standard Racket, for example with raco make. Furthermore, since DrRacket runs background expansion, such errors can be caught as early as the are written.

The following errors are raised at compile time:
> (define (foo bar [baz 'b] #:fizz fizz #:buzz [buzz #t])
    #true)
> (foo)

eval:17:0: foo: missing mandatory positional arguments

  header: (foo bar (baz) #:fizz (#:buzz))

  at: (foo)

  in: (foo)

> (foo 'a 'b 'c #:fizz 'f)

eval:18:0: foo: too many positional arguments

  header: (foo bar (baz) #:fizz (#:buzz))

  at: (foo 'a 'b 'c #:fizz 'f)

  in: (foo 'a 'b 'c #:fizz 'f)

> (foo 'a)

eval:19:0: foo: missing keywords

  header: (foo bar (baz) #:fizz (#:buzz))

  at: (foo 'a)

  in: (foo 'a)

> (foo 'a #:fizz 'f #:beurre 'b)

eval:20:0: foo: unknown keyword

  header: (foo bar (baz) #:fizz (#:buzz))

  at: #:beurre

  in: (foo 'a #:fizz 'f #:beurre 'b)

For the last error, DrRacket even highlights the wrong keyword.

4 Fail case🔗ℹ

When running randomized tests or processing unpredictable inputs, it can be difficult to reproduce a failure after the fact. The #:fail-case annotation on define helps with this by printing a copy-pastable call expression to current-error-port whenever an exn:fail? is raised during the function’s body.

Place #:fail-case between the function header and the body:

> (define (my-function x #:! y #:? [zoo 0] . rest)
    #:fail-case
    (when (= x y) (error "x and y must differ"))
    (list x y zoo rest))
> (my-function 1 #:y 2 #:zoo 3 'a 'b)

'(1 2 3 (a b))

> (my-function 6 #:y 6)

Reproducible failure case:

(my-function 6 #:y 6 #:zoo 0)

x and y must differ

If my-function raises an exn:fail?, a reproducible call expression is printed to current-error-port before the exception is re-raised. The output is designed to be copy-pastable into a REPL.

Notes:
  • Argument values are captured before the body executes, so mutations to arguments within the body do not affect the printed values.

  • Values are printed using ~v with print-as-expression set to #true. For values without a readable form, the output may need minor manual adjustment.

  • #:fail-case is not supported in curried function definitions such as (define ((f x) y) ...).

5 Struct2🔗ℹ

define2/struct2 provides struct2, an enhanced struct form that generates a keyword constructor <name>/kw using define2’s keyword syntax. This gives you compile-time checking of constructor calls (missing arguments, unknown keywords). struct2 supports inheritance in a slightly different (but more flexible) way than standard struct.

syntax

(struct2 name maybe-parent (field-spec ...) option ...)

 
maybe-parent = 
  | parent-id
     
field-spec = id
  | [id default-expr]
  | [id #:mutable]
  | [id default-expr #:mutable]
     
option = #:fender fender-expr
  | #:transparent
  | #:prefab
  | any struct option
Defines a struct like struct, plus a <name>/kw keyword constructor.

Example:
> (struct2 Point ([x 0] [y 0]) #:transparent)
> (Point/kw #:x 10 #:y 20)

(Point 10 20)

> (Point/kw #:y 5)

(Point 0 5)

> (Point/kw)

(Point 0 0)

5.1 Inheritance🔗ℹ

When a parent-id is given, the constructor takes a parent instance as its first positional argument and copies the parent’s fields into the new struct. The parent struct must be #:transparent. This allows both for inheritance and for extension of existing objects, and also allows for the fender to access the parent’s fields.

> (struct2 Shape ([color "red"]) #:transparent)
> (struct2 Circle Shape (radius) #:transparent)
> (define a-shape (Shape/kw #:color "blue"))
> (define a-circle (Circle/kw a-shape #:radius 5))
> (Shape-color a-circle)

"blue"

> (Circle-radius a-circle)

5

5.2 Fenders and super🔗ℹ

A #:fender clause adds validation that runs before construction. In an inheriting struct, the identifier super is automatically bound to the parent instance inside the fender — no import needed.

> (struct2 Shape2 ([color "red"]) #:transparent)
> (struct2 Disc Shape2 (radius)
           #:fender (when (<= radius 0)
                      (error 'Disc/kw "radius must be positive, got ~a" radius))
           #:transparent)
> (Disc/kw (Shape2/kw #:color "blue") #:radius 5)

(Disc "blue" 5)

> (Disc/kw (Shape2/kw) #:radius -1)

Disc/kw: radius must be positive, got -1

6 Acknowledgements🔗ℹ

Thanks to Ross Angle, Sorawee Porncharoenwase, Jack Firth, Jens-Axel Soegaard, Sam Tobin-Hochstadt, Greg Hendershott, Bogdan Popa, Matthew Flatt, Robby Findler, and Leif Anderson for their help.