On this page:
3.1 Interactive Tutorial
3.1.1 Installation
3.1.2 Setup
3.1.2.1 Dr  Racket
3.1.2.2 Emacs
3.1.2.3 Vim
3.1.3 Starting the Tutorial
3.2 Online Tutorial

3 Tutorial

This tutorial is available in two formats:

  1. An interactive format for you to go through and run yourself. (Recommended!)

  2. As standard Scribble documentation.

They contain similar material, but the interactive version includes additional steps and exercises. The interactive format is a more efficient and effective way to learn than reading documentation, and is recommended.

    3.1 Interactive Tutorial

      3.1.1 Installation

      3.1.2 Setup

        3.1.2.1 DrRacket

        3.1.2.2 Emacs

        3.1.2.3 Vim

      3.1.3 Starting the Tutorial

    3.2 Online Tutorial

3.1 Interactive Tutorial

This tutorial is distributed using the Racket Templates package, and contains the same material as the documentation-based tutorial, but also includes additional material such as exercises, all presented in an interactive format.

3.1.1 Installation

If you don’t already have Racket Templates installed, you’ll need to run this first:

raco pkg install from-template

And then, downloading the tutorial is as simple as:

raco new qi-tutorial
3.1.2 Setup

The tutorial is structured as a collection of Racket modules that you can interactively run, allowing you to experiment with each form as you hone your understanding. To do this most effectively, follow the instructions below for your chosen editor.

3.1.2.1 DrRacket

Laurent Orseau’s select-send-sexpr quickscript allows you to evaluate expressions on-demand in a context-sensitive way. It is essential for the interactive experience. Follow the instructions in the README to install it. Once installed, you can use Control-Shift-Enter (customizable) to evaluate the expression indicated (and usually highlighted) by your cursor position.

3.1.2.2 Emacs

The native Emacs experience in Racket Mode is already geared towards interactive evaluation, so you should be all set. If you use modal editing, however, I recommend trying Symex.el, which was designed with interactive evaluation in mind and provides a seamless experience here (disclosure: I’m the author!).

3.1.2.3 Vim

D. Ben Knoble’s tmux-vim-demo allows you to run expressions on demand with a split-pane view of your Vim buffer and a tmux session containing a Racket REPL. See the README for additional setup instructions once the package is installed. Once set up, you can simply use r (in Normal mode) to send the current line or visual selection to the REPL.

3.1.3 Starting the Tutorial

Open the file start.rkt in your favorite editor.

3.2 Online Tutorial

If you’d like to just go through the tutorial in documentation format, read on.

Qi is a general-purpose functional language, but it isn’t a #lang, it’s just a library. You can use it in any module just by:

> (require qi)

The basic way to write a flow is to use the form. A flow defined this way evaluates to an ordinary function, and you can pass input values to the flow by simply invoking this function with arguments.

Ordinary functions are already flows.

> (( sqr) 3)

9

Ordinary functions can be partially applied using templates.

> (( (+ 2)) 3)

5

... where underscores can be used to indicate argument positions.

> (( (string-append "a" _ "c")) "b")

"abc"

You can use flows anywhere that you would normally use a function (since flows are just functions). As an example, if you wanted to double every element in a list of numbers, you could use:

> (map ( (* 2)) (range 10))

'(0 2 4 6 8 10 12 14 16 18)

... rather than use currying:

> (map (curry * 2) (range 10))

'(0 2 4 6 8 10 12 14 16 18)

... or the naive approach using a lambda:

> (map (λ (v) (* v 2)) (range 10))

'(0 2 4 6 8 10 12 14 16 18)

👉 Flows are often more clear than using currying, and can also be preferable to using a lambda in many cases.

Literals are interpreted as flows generating them.

> (( "hello") 3)

"hello"

More generally, you can generate the result of any Racket expression as a flow by using gen (short for generate or "genesis" – to create or produce):

> (( (gen (+ 3 5))))

8

Flows like these that simply generate values always disregard any inputs you pass in.

> (( "hello") 3)

"hello"

> (( (gen (+ 3 5))) "a" "b" 'hi 1 2 3)

8

👉 gen is a common way to incorporate any Racket expression into a flow.

When an underscore is used as a flow (rather than in an argument position, as above), it is the "identity" flow, which simply passes its inputs through, unchanged.

> (( _) 3 4 5)

3

4

5

Sometimes, it’s useful to give flows a name, so that we can use them with different inputs in different cases. As flows evaluate to ordinary functions, we can name them the same way as any other function.

> (define double ( (* 2)))
> (double 5)

10

But Qi also provides a dedicated flow definition form so you can be more explicit that you are defining a flow, and then you don’t need to use .

> (define-flow double (* 2))
> (double 5)

10

Values can be "threaded" through multiple flows in sequence.

> (( (~> sqr add1)) 3)

10

More than one value can flow.

> (( (~> + sqr add1)) 3 5)

65

In Racket, if we wanted to evaluate an expression in terms of some inputs, we could wrap the expression in a lambda and immediately apply it to those input arguments:

> ((λ (x y)
     (add1 (sqr (+ x y))))
   3 5)

65

But usually, we’d just use the more convenient let form to do the same thing:

> (let ([x 3] [y 5])
    (add1 (sqr (+ x y))))

65

Qi provides an analogous form, on, which allows you to apply a flow immediately to inputs.

> (on (3 5)
    (~> + sqr add1))

65

Very often, the kind of flow that we want to apply immediately to inputs is a sequential one, i.e. a "threading" flow. So Qi provides an even more convenient shorthand for this common case.

> (~> (3 5) + sqr add1)

65

... which is similar to the widely used "threading macro," but is a more general version as it has access to all of Qi.

Flows may divide values.

> (( (-< add1 sub1)) 3)

4

2

> (( (-< + -)) 3 5)

8

-2

This -< form is called a "tee junction," named after a common pipe fitting used in plumbing to divide a flow down two pipes. It is also, of course, a Unix utility that performs a similar function for the input and output of Operating System processes.

Flows may channel values through flows in parallel.

> (( (== add1 sub1)) 3 7)

4

6

The == form is called a "relay." Think of it as a "relay race" where the values flow along parallel tracks. "Relay" is also a radio device that retransmits an input signal.

You could also pass all input values independently through a common flow.

> (( (>< sqr)) 3 4 5)

9

16

25

This >< form is called an "amp," analogous to "map" for lists, and also as it can be thought of as transforming or "amplifying" the inputs under some flow.

👉 Flows compose naturally, so that the entire Qi language is available to define each flow component within a larger flow.

( (~> (-< sqr (* 2) 1) +))

What do you think this flow computes? Take a minute to study it, see if you can work it out.

First, we see that the flow divides the input value down three flows that each transform the input in some way. The parallel outputs of these three flows are then fed into the addition flow, so that these results are added together. Note that the third branch in the tee junction is just the literal value, 1. Since it is a literal, it is interpreted (as we saw earlier) as a flow that generates that literal value, regardless of any inputs. So, the first two branches of the tee junction square and double the input, respectively, while the third branch simply outputs the constant value, 1. Putting it all together, this flow computes the formula x{}^2 + 2x + 1. Let’s apply it to an input using the threading shorthand:

> (~> (3) (-< sqr (* 2) 1) +)

16

The equivalent Racket expression is:

> (let ([x 3]) (+ (sqr x) (* x 2) 1))

16

Why would we favor the Qi version here? Well, we wouldn’t necessarily, but it has a few advantages: it doesn’t mention the input value at all, while the Racket version mentions it 3 times. It’s shorter. And most importantly, it encodes more information about the computation syntactically than the Racket version does. In what way? Well, with the Racket version, we don’t know what the expression is about to do with the input value. It might transform it, or it might condition on it, or it might disregard it altogether. We need to read the entire expression to determine the type of computation. With the Qi version, we can see that it is a sequential transformation just by looking.

Since we often work with lists in Racket, whereas we usually work with values in Qi, it is sometimes useful to separate an input list into its component values using a "prism."

> (( (~>  +)) (list 1 2 3))

6

... or constitute a list out of values using an "upside-down prism."

> (( (~> (>< sqr) )) 1 2 3)

'(1 4 9)

... in analogy with the effect that prisms have on light – separating white light into its component colors, and reconstituting them back into white light. Like this:

> (( (~>  )) (list 1 2 3))

'(1 2 3)

You can also swap the prisms the get an identity transformation on values, instead.

> (( (~>  )) 1 2 3)

1

2

3

Note that this isn’t exactly like the behavior with light, since with light, if you swapped the prisms their effect would be the same as before. There’s no such thing as an "upside down" prism in an absolute sense with light – the second one is "upside down" only in relation to the initial prism, and swapping the order of the prisms doesn’t change this aspect. The same prism may separate or combine light, just depending on where it is in the sequence.

With Qi prisms, though, and are different forms that do different things. separates, and collects. Therefore, they have a different effect when swapped, and, for instance, this would be an error:

> (( (~>  )) 1 2 3)

list?: arity mismatch;

 the expected number of arguments does not match the given

number

  expected: 1

  given: 3

... because △ cannot "separate" what is already separated – it expects a single input list.

👉 and often allow you to avoid using list and apply.

For instance:

> (~>> ((list 3 4 5)) (map sqr) (apply +))

50

This flow computes the sum of the squares of three values. We use map and apply here because the input happens to be in the form of a list. Instead, we could use a prism to separate the list into its component values, allowing us to use the flows on values directly:

> (~> ((list 3 4 5))  (>< sqr) +)

50

One way to think about flows is that they are a way to compose functions in complex ways. One type of function that we compose often is a predicate, that is, a boolean-valued function that answers a question about its inputs.

Predicates can be composed by using and, or, and not.

> (on (27)
    (and positive?
         integer?
         (~> (remainder 3) (= 0))))

#t

This answers whether the input is a positive integer divisible by 3, which, in this case, it is.

👉 As with any flow, we can give this one a name. In practice, this is an elegant way to define predicates.

> (define-flow threeish?
    (and positive?
         integer?
         (~> (remainder 3) (= 0))))
> (threeish? 27)

#t

> (threeish? 32)

#f

We often use predicates in conditional expressions such as if or cond. Since this pattern is so common, Qi provides a dedicated conditional form called switch which allows you to use flows as your conditions as well as the transformations to perform on the inputs if the conditions hold. This form is useful in cases where the result of the conditional expression is a function of its inputs. This is a very common case. Take a moment to scan through a favorite Racket project you worked on. Look for cond expressions where every condition answers a question about the same value or the same set of values. Every one of these cases (and more) are cases where you needed switch but didn’t have it. Well, now you do! Let’s look at what it does.

switch looks a lot like cond, except that every one of its condition and consequent clauses is a flow. These flows typically all receive the same inputs -– the original inputs to the switch expression.

> (switch (3)
    [positive? add1]
    [negative? sub1]
    [else _])

4

Let’s try this with a few different inputs. Instead of writing it from scratch each time, let’s give this flow a name. As we saw earlier, we could do this in the usual way with define:

> (define amplify
    ( (switch [positive? add1]
               [negative? sub1]
               [else _])))

... which also reveals how the switch form is just like ~> in that it is just a form of the Qi language. Since it represents another common case, Qi provides the shorthand switch form that we used above, which can be used at the Racket level alongside forms like cond, without having to enter Qi via .

Using define is one way to give this flow a name. Another way is to use the dedicated define-switch form provided by Qi, which is more explicit:

> (define-switch amplify
    [positive? add1]
    [negative? sub1]
    [else _])
> (amplify 3)

4

> (amplify -3)

-4

> (amplify 0)

0

As flows accept any number of input values, the predicates we define and use (for instance with switch) can operate on multiple values as well. The following flow computes the absolute difference between two input values:

> (define-values (a b) (values 3 5))
> (switch (a b)
    [> -]
    [< (~> X -)])

2

The X or "crossover" form used here reverses the order of the inputs, and ensures here that the larger argument is passed to the subtract operation in the first position.

Finally, we can end flows by using the ground form.

> (( ) 3 4 5)

This produces no values at all, and is useful in complex flows where we may wish to end certain branches of the flow based on predicates or some other criteria. As an example, the following flow sums all input numbers that are greater than 3.

> (( (~> (>< (if (> 3) _ ))
          +)) 1 3 5 2 7)

12

... which uses a flow version of if where the condition, consequent, and alternative expressions are all flows operating on the inputs. We could use a switch too, of course, but if is simpler here. As is the case with any complex language, there are many ways of saying the same thing in Qi. In this particular case, as it happens, Qi has a more convenient shorthand, pass, which only allows values through that meet some criterion.

> (( (~> (pass (> 3)) +)) 1 3 5 2 7)

12

We’ve now learned about the , on, ~>, and switch forms, which are ways to enter the Qi language. We’ve learned many of the forms of the Qi language and how they allow us to describe computations in terms of complex flows that direct multiple values through bifurcating and recombining sequences of transformations to arrive at the result.

We’ve also learned that we can give names to flows via define-flow, and define-switch.

Qi provides lots of other forms that allow you to express flows conveniently and elegantly in different contexts. You can see all of them in the grammar of the form, and they are individually documented under The Qi Language.

Next, let’s look at some examples to gain some insight into when to use Qi.