4 Language Interface
The most common way to use Qi is via interface macros in a host language such as Racket. Qi may also be used in tandem with other embedded or hosted DSLs.
4.1 Using Qi from the Host Language
The core entry-point to Qi from the host language is the form ☯. In addition, other forms such as on, switch, and ~> build on top of ☯ to provide convenient syntax in specialized cases. Together, these forms represent the interface between the host language (e.g. Racket) and Qi.
4.1.1 Core
syntax
(☯ flow-expr)
flow-expr =
| _ | (gen expr ...) | △ | sep | ▽ | collect | (esc expr) | (clos flow-expr) | (as identifier ...) | (one-of? expr ...) | (all flow-expr) | (any flow-expr) | (none flow-expr) | (and flow-expr ...) | (or flow-expr ...) | (not flow-expr) | (and% flow-expr ...) | (or% flow-expr ...) | NOT | ! | AND | & | OR | ∥ | NOR | NAND | XOR | XNOR | any? | all? | none? | inverter | ⏚ | ground | (~> flow-expr ...) | (thread flow-expr ...) | (~>> flow-expr ...) | (thread-right flow-expr ...) | X | crossover | == | (== flow-expr ...) | relay | (relay flow-expr ...) | (==* flow-expr ...) | (relay* flow-expr ...) | -< | (-< flow-expr ...) | tee | (tee flow-expr ...) | fanout | (fanout nat) | feedback | (feedback nat flow-expr) | (feedback nat (then flow-expr) flow-expr) | (feedback (while flow-expr) flow-expr) | (feedback (while flow-expr) (then flow-expr) flow-expr) | count | 1> | 2> | 3> | 4> | 5> | 6> | 7> | 8> | 9> | (select index ...) | (block index ...) | (bundle (index ...) flow-expr flow-expr) | (group nat flow-expr flow-expr) | sieve | (sieve flow-expr flow-expr flow-expr) | (partition [flow-expr flow-expr] ...) | (if flow-expr flow-expr) | (if flow-expr flow-expr flow-expr) | (when flow-expr flow-expr) | (unless flow-expr flow-expr) | switch | (switch switch-expr ...) | (switch (% flow-expr) switch-expr ...) | (switch (divert flow-expr) switch-expr ...) | (gate flow-expr) | >< | (>< flow-expr) | amp | (amp flow-expr) | pass | (pass flow-expr) | << | (<< flow-expr) | (<< flow-expr flow-expr) | >> | (>> flow-expr) | (>> flow-expr flow-expr) | (loop flow-expr) | (loop flow-expr flow-expr) | (loop flow-expr flow-expr flow-expr) | (loop flow-expr flow-expr flow-expr flow-expr) | (loop2 flow-expr flow-expr flow-expr) | (ε flow-expr flow-expr) | (effect flow-expr flow-expr) | apply | (qi:* expr ...) | (expr expr ... __ expr ...) | (expr expr ... _ expr ...) | (expr expr ...) | literal | identifier literal = boolean | char | string | bytes | number | regexp | byte-regexp | vector-literal | box-literal | prefab-literal | (quote value) | (quasiquote value) | (quote-syntax value) | (syntax value) expr = a-racket-expression index = exact-positive-integer? nat = exact-nonnegative-integer? switch-expr = [flow-expr flow-expr] | [flow-expr (=> flow-expr)] | [else flow-expr] identifier = a-racket-identifier value = a-racket-value
syntax
(flow flow-expr)
This produces a value that is an ordinary function. When invoked with arguments, this function passes those arguments as inputs to the defined flow, producing its outputs as return values. A flow defined in this manner does not name its inputs, and like any function, it only produces output when it is invoked with inputs.
See also on and ~>, which are shorthands to invoke the flow with arguments immediately.
syntax
(on (arg ...) flow-expr)
This is a way to pass inputs to a flow that is an alternative to the usual function invocation syntax (i.e. an alternative to simply invoking the flow with arguments). It may be preferable in certain cases, since the inputs are named at the beginning rather than at the end.
In the respect that it both defines as well as invokes the flow, it has the same relationship to ☯ as let has to lambda, and can be used in analogous ways.
Equivalent to ((☯ flow-expr) arg ...).
4.1.2 Threading
In these docs, we’ll sometimes refer to the host language as "Racket" for convenience, but it should be understood that Qi may be used with any host language.
Note that, as there may be any number of input arguments, they must be wrapped in parentheses in order to distinguish them from the flow specification – unlike the usual threading macro where the input is simply the first argument.
As flows themselves can be nonlinear, these threading forms too support arbitrary arity changes along the way to generating the result.
In the respect that these both define as well as invoke the flow, they have the same relationship to ☯ as let has to lambda, and can be used in analogous ways.
Equivalent to ((☯ (~> flow-expr ...)) args ...).
See also: Relationship to the Threading Macro.
> (~> (3) sqr add1) 10
> (~> (3) (-< sqr add1) +) 13
> (~> ("a" "b") (string-append "c")) "abc"
> (~>> ("b" "c") (string-append "a")) "abc"
> (~> ("a" "b") (string-append _ "-" _)) "a-b"
4.1.3 Conditionals
syntax
(switch (arg ...) maybe-divert-clause [predicate consequent] ... [else consequent])
Each of the predicate and consequent expressions is a flow, and they are each typically invoked with arg ..., so that the arguments need not be mentioned anywhere in the body of the form.
This Racket form leverages the identically-named Qi form. See Conditionals for its full syntax and behavior.
4.1.4 Lambdas
These anonymous function forms may be used in cases where you need to explicitly name the arguments for some reason. Otherwise, in most cases, just use ☯ directly instead as it produces a function while avoiding the extraneous layer of bindings.
syntax
(flow-lambda args flow-expr)
syntax
(flow-λ args flow-expr)
syntax
(π args flow-expr)
> ((flow-lambda a* _) 1 2 3 4) '(1 2 3 4)
> ((flow-lambda (a b c d) list) 1 2 3 4) '(1 2 3 4)
> ((flow-lambda (a . a*) list) 1 2 3 4) '(1 (2 3 4))
> ((flow-lambda (a #:b b . a*) list) 1 2 3 4 #:b 'any) '(1 (2 3 4))
> ((flow-lambda (a #:b b c . a*) list) 1 2 3 4 #:b 'any) '(1 2 (3 4))
> ((flow-lambda (a b #:c c) (~> + (* c))) 2 3 #:c 10) 50
syntax
(switch-lambda args maybe-divert-clause [predicate consequent ...] ... [else consequent ...])
syntax
(switch-λ args maybe-divert-clause [predicate consequent ...] ... [else consequent ...])
syntax
(λ01 args maybe-divert-clause [predicate consequent ...] ... [else consequent ...])
> ((switch-lambda (a #:b b . a*) [memq 'yes] [else 'no]) 2 2 3 4 #:b 'any) 'yes
> ((switch-lambda (a #:fx fx . a*) [memq (~> 1> fx)] [else 'no]) 2 2 3 4 #:fx number->string) "2"
> ((switch-lambda (x) [(and positive? odd?) (~> sqr add1)] [else _]) 5) 26
4.1.5 Definitions
The following definition forms may be used in place of the usual general-purpose define form when defining flows.
syntax
(define-flow name flow-expr)
syntax
(define-flow (head args) flow-expr)
syntax
(define-switch name maybe-divert-clause [predicate consequent ...] ... [else consequent ...])
syntax
(define-switch (head args) maybe-divert-clause [predicate consequent ...] ... [else consequent ...])
The advantage of using these over the general-purpose define form is that, as they express the definition at the appropriate level of abstraction and with the attendant constraints for the type of flow, they can be more clear and more robust, minimizing boilerplate while providing guardrails against programmer error.
4.2 Using the Host Language from Qi
Arbitrary native (e.g. Racket) expressions can be used in flows in one of two core ways. This section describes these two ways and also discusses other considerations regarding use of the host language alongside Qi.
4.2.1 Using Racket Values in Qi Flows
The first and most common way is to simply wrap the expression with a gen form while within a flow context. This flow generates the value of the expression.
4.2.2 Using Racket to Define Flows
The second way is if you want to describe a flow using the host language instead of Qi. In this case, use the esc form. The wrapped expression in this case must evaluate to a function, since functions are the only values describable in the host language that can be treated as flows. Note that use of esc is unnecessary for function identifiers since these are usable as flows directly, and these can even be partially applied using standard application syntax, optionally with _ and __ to indicate argument placement. But you may still need esc in the specific case where the identifier collides with a Qi form.
> (define-flow add-two (esc (λ (a b) (+ a b)))) > (~> (3 5) add-two) 8
4.2.3 Using Racket Macros as Flows
Flows are expected to be functions, and so you cannot naively use a macro as a flow. But there are many ways in which you can. If you’d just like to use such a macro in a one-off manner, see Converting a Macro to a Flow for an ad hoc way to do this. But a simpler and more complete way in many cases is to first register the macro (or any number of such macros) using define-qi-foreign-syntaxes prior to use.
syntax
(define-qi-foreign-syntaxes form ...)
> (define-syntax-rule (double-me x) (* 2 x)) > (define-syntax-rule (subtract-two x y) (- x y)) > (define-qi-foreign-syntaxes double-me subtract-two) > (~> (5) (subtract-two 4) double-me) 2
> (~>> (5) (subtract-two 4) double-me) -2
> (~> (5 4) (subtract-two _ _) double-me) 2
By doing this, you can thread multiple values through such syntaxes in the same manner as functions. The catch-all template __ isn’t supported though, since macros (unlike functions) require all the "arguments" to be supplied syntactically at compile time. So while any number of arguments may be supplied to such macros, it must be a specific rather than an arbitrary number of them, which may be indicated syntactically via _ to indicate individual expected arguments and their positions.
Note that for a foreign macro used in identifier form (such as double-me in the example above), it assumes a single argument. This is different from function identifiers where they receive as many values as may happen to be flowing at runtime. With macros, as we saw, we cannot provide them an arbitrary number of arguments. If more than one argument is anticipated, explicitly indicate them using a _ template.
Finally, as macros "registered" in this way result in the implicit creation of Qi macros corresponding to each foreign macro, if you’d like to use these forms from another module, you’ll need to provide them just like any other Qi macro, i.e. via (provide (for-space qi ...)).
4.3 Using Qi with Another DSL
Qi may also be used in tandem with other DSLs in a few different ways – either directly, if the DSL is implemented simply as functions without custom syntax, or via a one-to-one macro "bridge" between the two languages (if the interface between the languages is small), or potentially by implementing the DSL itself as a Qi dialect (if the languages interact extensively).
4.3.1 Using Qi Directly
If the forms of the DSL are callable, i.e. if they are functions, then you can just use Qi with them the same way as with any other function.
4.3.2 Using a Macro Bridge
See Converting a Macro to a Flow.
Using the macro bridge approach, you would need to write a corresponding Qi macro for every form of your DSL that interacts with Qi (or use define-qi-foreign-syntaxes to do this for you). If this interface between the two languages is large enough, and their use together frequent enough, this approach too may prove cumbersome. In such cases, it may be best to implement the DSL itself as a dialect of Qi.
4.3.3 Writing a Qi Dialect
The problem with the macro bridge approach is that all paths between the two languages must go through a level of indirection in the host language. That is, the only way for Qi and the other DSL to interact is via Racket as an everpresent intermediary.
To get around this, a final possibility to consider is to translate the DSL itself so that it’s implemented in Qi rather than Racket. That is, instead of being specified using Racket macros via e.g. define-syntax-parse-rule and define-syntax-parser, it would rather be defined using define-qi-syntax-rule and define-qi-syntax-parser so that the language expands to Qi rather than Racket (directly). This would allow your language to be used with Qi seamlessly since it would now be a dialect of Qi.
There are many kinds of languages that you could write in Qi. See Writing Languages in Qi for a view into the possibilities here, and what may be right for your language.