mixfix
1 Overview
2 Operator management
3 Caveats
3.1 Try order
3.2 Unintentional yielding
3.3 Interaction with module+ submodules
4 Tips & Tricks
4.1 Using an identifier macro at a head position
4.2 Custom function application integration
5 Performance
6 Reference
define-mixfix
yield-mixfix
#%app
define-mixfix-rule
import-mixfix
define-mixfix-set
define-literal
7 Gallery
7.1 Parenthesis shape
7.2 Mixfix operators within mixfix operators
8 Acknowledgements
8.0

mixfix

Sorawee Porncharoenwase <sorawee.pwase@gmail.com>

 (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?)
Defines a mixfix operator that associates with transformer-expr. If name-id is given, additionally binds name-id to the mixfix operator, so that it can be provided or used in a mixfix operator set.

procedure

(yield-mixfix)  any

Yields the control to other mixfix operators. This binding is provided for-syntax.

syntax

#%app

Tries invoking various mixfix operators. This form is used implicitly at every function application. Users do not need to use it directly.

syntax

(define-mixfix-rule pattern maybe-option pattern-directive ... template)

Defines a mixfix operator via pattern matching à la define-syntax-parse-rule. When the pattern fails to match, the mixfix operator will automatically yield.

syntax

(import-mixfix id ...)

Imports ids which must be identifiers bound to a mixfix operator or a mixfix operator set in the specified order.

syntax

(define-mixfix-set name-id (id ...))

Binds name-id to a mixfix operator set that contains ids which must be identifiers bound to a mixfix operator or a mixfix operator set in the specified order.

syntax

(define-literal id transformer-expr)

 
  transformer-expr : (-> syntax? syntax?)
Defines id as an identifier macro that cooperates with mixfix operators. The identifier macro is associated with transformer-expr.

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.