Polysemy: support for polysemic identifiers
(require polysemy) | package: polysemy |
This is an experimental proof of concept, and is not intended to be used in production until the potential issues of doing so have been discussed with other racketeers.
The bindings described here may be changed in future versions without notice.
1 Examples
This first example shows four short modules which all define the identifier ^, with four different meanings: the first uses it as a special token (similarly to the use of : to separate fields from their type in Typed Racket, among other things); the second defines it as a exclusive-or match expander; the third defines it as the exponentiation function; the fourth defines it as the two-variable logical xor function (which, thankfully, does not need any short-circuiting behaviour).
> (module m-one racket (require polysemy (for-syntax syntax/parse racket/list)) (provide (poly-out [my-macro normal-macro] [^ my-macro-repeat-n-times])) (define-poly-literal ^ my-macro-repeat-n-times hat-stxclass) (define-poly my-macro normal-macro (syntax-parser [(_ v :hat-stxclass n) #`(list . #,(for/list ([i (in-range (syntax-e #'n))]) #'v))])))
> (module m-two racket (require polysemy (for-syntax syntax/parse)) (provide (poly-out [[xor ^] match-expander])) (define-poly xor match-expander (syntax-parser [(_ a b) #'(and (or a b) (not (and a b)))])))
> (module m-three racket (require polysemy) (provide (all-defined-out)) ; Multi-argument functions are not supported yet… (define-poly-case (^ [x number?]) (λ (y) (expt x y))))
> (module m-four racket (require polysemy) (provide (all-defined-out)) (define-poly-case (^ [x boolean?]) (λ (y) (and (or x y) (not (and x y)))))) ; Seamlessly require the two versions of ^ > (require 'm-one 'm-two 'm-three 'm-four racket/match) > (my-macro 'foo ^ 3) '(foo foo foo)
> (match "abc" [(^ (regexp #px"a") (regexp #px"b")) "a xor b but not both"] [_ "a and b, or neither"]) "a and b, or neither"
> ((^ 2) 3) 8
> ((^ #t) #f) #t
Thanks to the use of polysemy, all four uses are compatible, and it is possible to require the four modules without any special incantation at the require site. The providing modules themselves have to use special incantations, though: define-poly-literal, define-poly and define-poly-case. Furthermore, a simple rename-out does not cut it anymore, and it is necessary to use poly-out to rename provided polysemic identifiers. Note that a static check is performed, to make sure that the cases handled by ^ from m-three do not overlap the cases handled by ^ from m-four. The function overloads are, in this sense, safe.
The following example shows of the renaming capabilities of polysemy: three meanings for the foo identifier are defined in two separate modules (two meanings in the first, one in the second). The meanings of foo from the first module are split apart into the identifiers baz and quux, and the meaning from the second module is attached to baz. The identifier baz is therefore a chimera, built with half of the foo from the first module, and the foo from the second module.
> (module ma racket (require polysemy) (provide (all-defined-out)) (define-poly foo match-expander (λ (stx) #'(list _ "foo" "match"))) (define-poly-case (foo [x integer?]) (add1 x)))
> (module mb racket (require polysemy) (provide (all-defined-out)) (define-poly-case (foo [x list?]) (length x))) ; baz is a hybrid of the foo match expander from ma, ; and of the foo function on lists from mb. ; ma's foo function is separately renamed to quux.
> (require polysemy racket/match (poly-rename-in 'ma [[foo baz] match-expander] [[foo quux] (case-function integer?)]) (poly-rename-in 'mb [[foo baz] (case-function list?)])) ; baz now is a match expander and function on lists: > (match '(_ "foo" "match") [(baz) 'yes]) 'yes
> (baz '(a b c d)) 4
; The baz function does not accept integers ; (the integer-function part from ma was split off) > (baz 42) baz: contract violation
expected: list?
given: 42
in: the 1st argument of
(-> (listof any/c) any)
contract from: (function baz)
blaming: top-level
(assuming the contract is correct)
at: eval:6:0
; The quux function works on integers… > (quux 42) 43
; … but not on lists, and it is not a match expander > (quux '(a b c d)) quux: contract violation
expected: integer?
given: '(a b c d)
in: the 1st argument of
(-> integer? any)
contract from: (function quux)
blaming: top-level
(assuming the contract is correct)
at: eval:8:0
> (match '(_ "foo" "match") [(quux) 'yes] [_ 'no]) eval:9:0: match: syntax error in pattern
in: (quux)
2 Introduction
This module allows defining polysemic identifiers which can act as a match expander, as a macro, as an identifier macro, as a set! subform, and as a collection of function overloads.
The following meanings are special:
The value for the normal-macro meaning is used when the identifier appears as the first element of a macro application (i.e. when it is used as a as a macro).
The value for the identifier-macro meaning is used when the identifier appears on its own as an expression (i.e. when it is used as an identifier macro).
The value for the match-expander meaning is used when the identifier is used as a match template
The value for the 'set!-macro meaning is used when the identifier is appears as the second element of a set! form.
Other "core" meanings may be added later, and third-party libraries can define their own meanings.
3 Bindings provided by polysemy
In all the forms below, the meaning should be a simple identifier. Note that is lexical context is not taken into account (i.e. the comparison is based on the equality of symbols, not based on free-identifier=?), and therefore every meaning should be globally unique. Later versions may add a notion of hygiene to meanings (allowing these meanings themselves to be renamed, to circumvent conflicts).
require transformer
(poly-only-in module [maybe-rename meaning ...] ...)
maybe-rename = old-id | [old-id new-id]
require transformer
(poly-rename-in module [maybe-rename meaning] ...)
maybe-rename = old-id | [old-id new-id]
provide transformer
(poly-out module [maybe-rename meaning])
maybe-rename = old-id | [old-id new-id]
If old-id and new-id are supplied, each given meaning, which must be attached to old-id, will be re-attached to new-id.
syntax
(define-poly id)
(define-poly id meaning value)
The second form attaches the phase 1 value (i.e. it is a transformer value) to the given meaning of the id.
pattern expander
(~poly pvar meaning)
The transformer value for the requested meaning is stored in the value attribute.
syntax
(define-poly-literal id meaning syntax-class)
This can be used to define "tokens" for macros, which bear a special meaning for some macros, but might have a different meaning for another third-party macro. If both rely on polysemy, then they can use the same default name, without the risk of the identifiers conflicting. Furthermore, it is possible to rename the two meanings separately.
syntax
(define-poly-case (name [arg₀ pred?]) . body)
Defines an overload for the name function, based on the type of its first argument. For now, only a few contracts are allowed:
When any polysemic identifier which is contains a poly-case is called as a function, a check is performed to make sure that none of its cases overlap. If some cases overlap, then an error is raised.
Note that an identifier cannot have both a meaning as a function case, and a normal-macro or identifier-macro meanings.
poly-meaning-expander
(case-function pred?)
4 Limitations
There are currently many limitations. Here are a few:
Meanings themselves cannot be renamed, and must therefore be globally unique. A later version could solve this by generating the actual meaning symbol using gensym, and by attaching it to a user-friendly name by means of a poly-meaning-expander.
It should be possible to specify multiple macro cases, as long as they do not overlap.
Function overloads currently only allow a single argument. Adding multiple dispatch and multiple non-dispatch arguments would be nice.
Only a few contracts are supported by function overloads. For simple contracts, it is only a matter of extending the inheritance table in "ids.rkt". More complex contract combinators will require a bit more work.
The generated functions are not compatible with Typed Racket. Deriving types from the small set of contracts that we support should not be difficult, and would allow function overloads in Typed Racket (as long as the user-defined functions are typed, of course).
The whole contraption relies on marshalling names. Since require and provide only care about plain names, and do not have a notion of scopes (which could be used to hide some of these names), I do not see any way to avoid this problem, while still making simple imports (i.e. without renaming) work seamlessly with the stock implementation of require.
There is no support for polysemic identifiers in Scribble: identifiers will not get highlighted, and raco doc-coverage will complain that some internal identifiers are not documented (using raco doc-coverage -s ’^ ’ module-path is a quick workaround for this second issue).