Qi:   A Functional, Flow-Oriented DSL
1 Introduction
2 Syntax
3 Forms
flow
on
~>
~>>
switch
flow-lambda
π
switch-lambda
λ01
define-flow
define-switch
4 Usage
8.2

Qi: A Functional, Flow-Oriented DSL

Siddhartha Kasivajhula

 (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.

Examples:
> (( (and positive? odd?)) 5)

#t

> (( (<= 5 _ 10)) 6)

#t

> (( (<= 5 _ 10)) 12)

#f

> (( (~> (>< ->string) string-append)) 5 7)

"57"

> (define-switch (abs n)
    [negative? (* -1)]
    [else _])
> (abs -5)

5

> (abs 5)

5

> (define-flow ( m n)
    (~> - abs (< 1)))
> ( 5 7)

#f

> ( 5 5.4)

#t

> (define-flow (root-mean-square vs)
    (~>> (map sqr) (-< sum length) / sqrt))
> (root-mean-square (range 10))

5.338539126015656

1 Introduction

A flow is either made up of flows, or is a native (e.g. Racket) function. Flows may be composed using a number of combinators that could yield either linear or nonlinear composite flows.

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.

2 Syntax

This section provides a specification of the basic syntax recognizable to all of the forms provided in this module.

3 Forms

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.

syntax

( flow-expr)

 
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)

syntax

(flow ...)

Define a flow.

Example:
> (( (and positive? odd?)) 5)

#t

syntax

(on (args ...) flow-expr)

Define a flow with the inputs named in advance.

Typically, on should only be used for the general case of evaluating an expression in the context of a pre-defined subject (such as while defining a predicate). For the more specific case of predicate-based dispatch, use switch.

Example:
> (on (5) (and positive? odd?))

#t

syntax

(~> (args ...) flow-expr ...)

syntax

(~>> (args ...) flow-expr ...)

Thread inputs through a sequence of flows. ~> threads arguments in the first position by default, while ~>> uses the last position, but in either case the positions can instead be explicitly indicated by using _.

As flows themselves can be nonlinear, these threading forms too support arbitrary arity changes along the way to generating the result.

Examples:
> (~> (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"

syntax

(switch (args ...)
  [predicate consequent ...]
  ...
  [else consequent ...])
A predicate-based dispatch form, usable as an alternative to cond and if.

Examples:
> (switch (5)
    [(and positive? odd?) (~> sqr add1)]
    [else _])

26

> (switch (2 3)
    [< +]
    [else min])

5

syntax

(flow-lambda args body ...)

syntax

(π args body ...)

Similiar to lambda but constrained to the flow language. This is exactly equivalent to (lambda args (on (args) body ...)). π is an alias for flow-lambda.

syntax

(switch-lambda (args ...)
  [predicate consequent ...]
  ...
  [else consequent ...])

syntax

(λ01 (args ...)
  [predicate consequent ...]
  ...
  [else consequent ...])
Similar to lambda but constrained to be a (predicate-based) dispatcher. This is exactly equivalent to (lambda args (switch (args) [predicate consequent ...] ... [else consequent ...])). λ01 is an alias for switch-lambda.

Example:
> ((switch-lambda (x)
     [(and positive? odd?) (~> sqr add1)]
     [else _]) 5)

26

syntax

(define-flow (name args) body ...)

Similiar to the function form of define but constrained to the flow language. This is exactly equivalent to (define name (lambda/subject args body ...)).

syntax

(define-switch (args ...)
  [predicate consequent ...]
  ...
  [else consequent ...])
Similiar to the function form of define but constrained to be a (predicate-based) dispatcher. This is exactly equivalent to (define name (switch-lambda args [predicate consequent ...] ... [else consequent ...])).

4 Usage

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.