|(require qi)||package: qi|
An embedded, general-purpose language to allow convenient framing of programming logic in terms of functional flows.
One way to structure computations is as a flowchart, with arrows representing logical transitions indicating the sequence in which actions are performed. Aside from this ordering, these actions are independent of one another and could be anything at all. This is the standard model we implicitly employ when writing functions in Racket or another programming language. The present module provides another way, where computations are structured as a flow like a flow of energy, electricity passing through a circuit, rivers flowing through channels. In this model, arrows represent that the outputs of one flow feed into another. This constraint allows us to compose functions at a high level to derive complex and robust functional pipelines from simple components with a minimum of repetition and boilerplate, engendering effortless clarity.
> ((☯ (and positive? odd?)) 5)
> ((☯ (<= 5 _ 10)) 6)
> ((☯ (<= 5 _ 10)) 12)
> ((☯ (~> (>< ->string) string-append)) 5 7)
> (define-switch (abs n) [negative? (* -1)] [else _]) > (abs -5)
> (abs 5)
> (define-flow (≈ m n) (~> - abs (< 1))) > (≈ 5 7)
> (≈ 5 5.4)
> (define-flow (root-mean-square vs) (~>> (map sqr) (-< sum length) / sqrt)) > (root-mean-square (range 10))
The flow gen allows an ordinary value to be "lifted" into a flow – thus, any value can be incorporated into flows.
The semantics of a flow is function invocation – simply invoke a flow with inputs (i.e. ordinary arguments) to obtain the outputs. A flow in general is n × m, i.e. it accepts n inputs and yields m outputs, for arbitrary non-negative integers m and n.
This section provides a specification of the basic syntax recognizable to all of the forms provided in this module.
The core form that defines and uses the flow language is ☯, while other forms such as on, switch, and ~> leverage the former to provide convenient syntax in specialized cases. on provides a way to declare the arguments to the flow up front. ~> is similar to on but implicitly threads the arguments through a sequence of flows. switch is a conditional dispatch form analogous to cond whose predicate and consequent expressions are all flows. In addition, other forms like define-flow and define-switch are provided that leverage these to create functions constrained to the flow language, for use in defining predicates, dispatchers, or arbitrary transformations. The advantage of using these forms over the usual general-purpose define form is that they are more clear and more robust, as the constraints they impose minimize boilerplate by narrowing scope, while also providing guardrails against programmer error.
flow-expr = _ | (one-of? flow-expr) | (all flow-expr) | (any flow-expr) | (none flow-expr) | (and flow-expr) | (or flow-expr) | (not flow-expr) | (gen flow-expr) | (NOT flow-expr) | (AND flow-expr) | (OR flow-expr) | (NOR flow-expr) | (NAND flow-expr) | (XOR flow-expr) | (XNOR flow-expr) | (and% flow-expr) | (or% flow-expr) | (~> flow-expr) | (thread flow-expr) | (~>> flow-expr) | (thread-right flow-expr) | (any? flow-expr) | (all? flow-expr) | (none? flow-expr) | (X flow-expr) | (crossover flow-expr) | (>< flow-expr) | (amp flow-expr) | (pass flow-expr) | (== flow-expr) | (relay flow-expr) | (-< flow-expr) | (tee flow-expr) | (select flow-expr) | (group flow-expr) | (sieve flow-expr) | (if flow-expr) | (switch flow-expr) | (gate flow-expr) | (ground flow-expr) | (fanout flow-expr) | (feedback flow-expr) | (inverter flow-expr) | (effect flow-expr) | (collect flow-expr) | (apply flow-expr) | (esc flow-expr) | (val:literal flow-expr) | (quote flow-expr) | ((__) flow-expr) | ((_) flow-expr) | (() flow-expr) | (ex flow-expr) | (_ flow-expr)
(on (args ...) flow-expr)
As flows themselves can be nonlinear, these threading forms too support arbitrary arity changes along the way to generating the result.
> (~> (3) sqr add1)
> (~> (3) (-< sqr add1) +)
> (~> ("a" "b") (string-append "c"))
> (~>> ("b" "c") (string-append "a"))
> (~> ("a" "b") (string-append _ "-" _))
(switch (args ...) [predicate consequent ...] ... [else consequent ...])
(switch-lambda (args ...) [predicate consequent ...] ... [else consequent ...])
(λ01 (args ...) [predicate consequent ...] ... [else consequent ...])
(define-flow (name args) body ...)
(define-switch (args ...) [predicate consequent ...] ... [else consequent ...])
The Qi language isn’t specific to a domain (except the domain of functions!) and may be used in normal (e.g. Racket) code simply by employing the appropriate form.
Arbitrary native (e.g. Racket) expressions can be used in flows in one of two ways. 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. The second way is if you want to describe a flow using the native language instead of the flow language. 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 native 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 it in the specific case where the identifier collides with a Qi form.