mixfix
(require mixfix) | package: mixfix |
This library allows users to define and use mixfix operators in Racket.
1 Overview
The Racket language uses the prefix notation: in a macro invocation (m x ....), the macro operator m must be an identifier located at the head position, and bound to a syntax transformer.
The mixfix notation, on the other hand, allows a macro invocation to have multiple operator tokens in arbitrary position (or even no operator token at all!). For example, one can define the conditional ternary operator via define-mixfix-rule and then use it as follows:
> (require (for-syntax racket/base syntax/parse))
> (define-mixfix-rule (c {~datum ?} t {~datum :} e) (if c t e)) > (#true ? 1 : 2) 1
Unlike regular macros, syntax transformers in this system are (by default) not associated with any identifier. Instead, a macro invocation simply tries every syntax transformer from various mixfix operators that are in scope, following the ordering that respects the scope hierarchy.
> (let ([x 42]) ; "Shadows" the previous conditional ternary operator (define-mixfix-rule (c {~datum ?} t {~datum :} e) (+ x (if c t e))) (#true ? 1 : 2)) 43
; The original operator is "restored" outside of let > (#true ? 1 : 2) 1
While define-mixfix-rule is convenient, the library provides the more general define-mixfix which allows users to specify an arbitrary syntax transformer. Using define-mixfix, users need to manually yield the control to other operators via yield-mixfix when the mixfix operator does not want to transform the input syntax.
> (define-mixfix (Ξ» (stx) (syntax-parse stx [({~datum the} {~datum meaning} {~datum of} {~datum life}) #'42] [_ (yield-mixfix)]))) > (the meaning of life) 42
; The "meaning of life" operator is tried, ; but it yields (to the regular function application) > (+ 1 2 3) 6
Mixfix operators, regular macros, and core forms can coexist. However, regular macros and core forms will have a higher precedence over mixfix operators.
> (define-mixfix-rule (x {~datum ++}) (add1 x)) > (1337 ++) 1338
; quote-syntax takes control here > (quote-syntax ++) #<syntax:eval:12:0 ++>
2 Operator management
Similar to regular macros, mixfix operators can be imported and exported from a module. Every mixfix operator defining form supports the #:name option. The given identifier will be associated with the mixfix operator, allowing users to provide the identifier from a module. It is recommended that the given identifier is not used for any other purpose.
> (module submodule racket (require mixfix) (define seven 7) (define-mixfix-rule (x #:+ y) #:name +-operator (+ seven x y)) (define-mixfix-rule (x #:* y) #:name *-operator (* -1 x y)) (provide +-operator *-operator))
Users can then use import-mixfix to import mixfix operators in the specified order.
> (require 'submodule)
> (let () (import-mixfix +-operator *-operator) (list (1 #:+ 2) (3 #:* 4))) '(10 -12)
Specifying many mixfix operators to import can be cumbersome. Therefore, the library allows users to group mixfix operators together as a mixfix operator set via define-mixfix-set. Importing a mixfix operator set will import every mixfix operator in the set in the specified order.
> (define-mixfix-set arithmetic-operator-set (+-operator *-operator))
> (let () (import-mixfix arithmetic-operator-set) (list (1 #:+ 2) (3 #:* 4))) '(10 -12)
3 Caveats
3.1 Try order
Mixfix operators are discovered as the macro expander expands the program. When several mixfix operators are defined (or imported) in the same scope level, those that are defined later will be tried first.
> (define-mixfix-rule (#:x a) #:name x-operator (add1 a)) ; x-operator is tried and used. > (#:x 99) 100
> (define-mixfix-rule (#:x 99) #:name y-operator 0) ; y-operator is tried and yielded. ; x-operator is tried and used. > (#:x 42) 43
; y-operator is tried and used. > (#:x 99) 0
The interaction of this behavior and partial expansion in a module context or an internal-definition context could lead to a surprising outcome, however. Therefore, mixfix operators should be carefully designed and defined.
> (module another-submodule racket (require mixfix) ; (1) x-operator is discovered. (define-mixfix-rule (#:x a) #:name x-operator (add1 a)) ; (2) x-operator is used. (#:x 99) ; (3) The function application is partially expanded. (values ; (5) The arguments are expanded. ; y-operator is used. (#:x 99)) ; (4) y-operator is discovered. (define-mixfix-rule (#:x 99) #:name y-operator 0)) > (require 'another-submodule)
100
0
3.2 Unintentional yielding
When using define-mixfix-rule, users need to be careful that the pattern matching failure will not result in an unintentional yielding.
As an example, users might want to create a shorthand lambda notation as follows:
> (define-mixfix-rule ({~and arg:id {~not {~datum :}}} ... {~datum :} body:expr) (Ξ» (arg ...) body)) > (define f (x : (y : (+ x y)))) > ((f 1) 2) 3
But when the lambda notation is ill-formed, the operator would yield to next operators (function application in this case). Unintentional yielding creates an obscure error at best and incorrect program at worst.
> (define g (x : x 1)) x: undefined;
cannot reference an identifier before its definition
in module: top-level
One possible solution to this problem is to use the cut (~!) operator from syntax/parse, which can be used to commit the parsing.
> (define-mixfix-rule ({~and arg:id {~not {~datum :}}} ... {~datum :} ~! body:expr) (Ξ» (arg ...) body)) > (define f (x : (y : (+ x y)))) > ((f 1) 2) 3
> (define g (x : x 1)) eval:32:0: x: unexpected term
at: 1
in: (x : x 1)
3.3 Interaction with module+ submodules
A mixfix operator defined in a module cannot be used in module+ submodules.
> (module foo racket (require mixfix) (define-mixfix-rule (#:x) 42) (module+ test (#:x))) eval:33:0: #%datum: keyword misused as an expression
at: #:x
The issue can be workaround by using import-mixfix.
> (module foo racket (require mixfix) (define-mixfix-rule (#:x) #:name x-op 42) (module+ test (import-mixfix x-op) (#:x))) > (require (submod 'foo test)) 42
4 Tips & Tricks
4.1 Using an identifier macro at a head position
An identifier macro can be used along with mixfix operators. However, when it is used at a head position, it becomes a regular macro invocation with a higher precedence over mixfix operations.
> (define-syntax (this stx) #'42)
> (define-mixfix-rule (x {~datum +} y) (+ x y)) > (99 + this) 141
; Unexpected result > (this + 99) 42
To make the identifier macro cooperate with mixfix operators properly, users need to expand the identifier macro to the function application form provided by the mixfix library when it is at a head position. The mixfix library provides define-literal to help automating this process.
> (define-literal this (Ξ» (stx) #'42))
> (define-mixfix-rule (x {~datum +} y) (+ x y)) > (99 + this) 141
> (this + 99) 141
4.2 Custom function application integration
Several Racket libraries override #%app. Users may wish to use these libraries along with the mixfix system. Unfortunately, a straightforward attempt will not work because the mixfix system also overrides #%app, causing a conflict. However, users can instead create a mixfix operator that always transforms the input syntax with these librariesβ #%app, thus achieving the same effect.
> (module very-fancy-app racket ; Flip all arguments (provide (rename-out [$#%app #%app])) (require syntax/parse/define) (define-syntax-parser $#%app [(_ x ...) #`(#%app #,@(reverse (syntax->list #'(x ...))))])) ; Rename #%app > (require (only-in 'very-fancy-app [#%app very-fancy-app:#%app])) ; Make the fallback mixfix operator catches everything
> (define-mixfix-rule (arg ...) (very-fancy-app:#%app arg ...))
> (define-mixfix-rule (c {~datum ?} t {~datum :} e) (if c t e)) > (#true ? 1 : 2) 1
> (12 34 -) 22
5 Performance
The flexibility that this library provides comes at the cost of performance. However, it will only affect compile-time performance. The run-time performance is completely unaffected.
6 Reference
syntax
(define-mixfix maybe-option transformer-expr)
maybe-option =
| #:name name-id
transformer-expr : (-> syntax? syntax?)
procedure
(yield-mixfix) β any
syntax
syntax
(define-mixfix-rule pattern maybe-option pattern-directive ... template)
syntax
(import-mixfix id ...)
syntax
(define-mixfix-set name-id (id ...))
syntax
(define-literal id transformer-expr)
transformer-expr : (-> syntax? syntax?)
7 Gallery
7.1 Parenthesis shape
> (define-mixfix-rule (x {~seq {~literal +} xs} ...+) #:when (eq? (syntax-property this-syntax 'paren-shape) #\{) (+ x xs ...)) > (apply + '(1 2 3)) 6
> {4 + 5 + 6} 15
7.2 Mixfix operators within mixfix operators
> (define-mixfix-rule ({~datum $} ~! . _) #:fail-when #true "unknown testing form" (void))
> (define-mixfix-rule ({~datum $} x {~datum is} y {~datum because-of} z) (let ([x* x] [y* y] [z* z]) (unless (equal? x* y*) (raise-arguments-error 'test "not equal" "expected" y* "got" x*)) (unless (equal? z* y*) (raise-arguments-error 'test "wrong explanation" "expected" y* "explanation" z*))))
> (define-mixfix-rule ({~datum $} x {~datum is} y) (let ([y* y]) ($ x is y* because-of y*)))
> (define (times-two x) (+ x x)) > ($ (times-two 3) is 6) > ($ (times-two 3) is 7) test: not equal
expected: 7
got: 6
> ($ (times-two 3) is 6 because-of (* 2 3)) > ($ (times-two 3) is 6 because-of (+ 2 3)) test: wrong explanation
expected: 6
explanation: 5
> ($ (times-two 3) is 6 because-of) eval:376:14: $: unknown testing form
at: ($ (times-two 3) is 6 because-of)
in: ($ (times-two 3) is 6 because-of)
8 Acknowledgements
I would like to thank Ross Angle, Shu-Hung You, and Sam Tobin-Hochstadt for the discussion on the limitation of mixfix operators in module+ submodules.