Learn to program with Wisp

Wisp is a programming language with clear, readable syntax, which executes as Scheme, the most advanced language in use today; built on the concept of removing limitations from the language to give freedom to programmers, it allows you to transcend even the limits of the imagination of its creators, as shown for example by the lightweight concurrency with fibers, or object oriented programming with GOOPS.

In this tutorial you will learn to write programs with wisp. It requires no prior knowledge of programming.

Your first program (hello world)

To start with a system which allows you to transgress tradition, let’s begin with tradition: Hello World

display "Hello World!\n" .
Hello World!

To execute the above yourself, first install Guile 2.x and wisp.

Now you can run guile -x .w --language=wisp.1 This gives you a live wisp shell. You should now see

wisp@(guile-user)>

Type the code from the start of this chapter into the shell; where you see wisp@(guile-user)>:

wisp@(guile-user)> display "Hello World!\n" .

It should now display the following:

Hello World!

That’s it, your first Wisp program!

If you missed the trailing period, you need to hit enter two times to run the code.

Understanding this Hello World

When you start with or interactive use, it shows an input prompt with the prefix wisp@(guile-user)>.

The line you entered consists of three logical parts, separated by spaces.

  • The first word is a function which gets called (display displays its first argument),2
  • the second is the argument to the function ("Hello World!\n"). This has some details to explain:
    • the argument provided ("Hello World!\n") is a string (text) enclosed in double quotes ("").
    • Everything between the two double quotes is part of the string, spaces in the string do not separate arguments.
    • The "\n" is an escaped newline. In general a backslash (\) in a string starts an escape sequence which allows adding special elements like nonprinting characters (the full list of escape sequences is available in the Guile manual). To add the backslash itself, use \\.

The escaped newline makes wisp display a newline, so the Hello World! is shown on its own line. Without the newline, the new input prompt would be appended to the Hello World!:

wisp@(guile-user)> display "Hello World!" .
Hello World!wisp@(guile-user)>

The third part of the code is the trailing period, separated by a space or linebreak. Like a period in prose ends a sentence, the trailing period tells wisp to run your input right away. Without that period, you need to add two empty lines to end and run the input.3 The interactive prompt shows three dots at the beginning of each of these empty lines.

wisp@(guile-user)> display "Hello World!\n"
... 
...
Hello World!
wisp@(guile-user)>

To sum it up: The first word on a line is a function to call. The rest of the line are arguments to the call. A trailing period ends the call like two empty lines.

From now on we will omit wisp@(guile-user)> and the ... and just show the raw code you type.

Hello math: functions calling functions

To display the result of other functions, use them as child-lines: more deeply indented lines. An example is calculating the square root of a number:

display
    sqrt 4
2

Typical math symbols can be used as functions:

display
    + 2 2
4

And you can nest an arbitrary number of function calls:

display
    sqrt
        + 2 2
2

Summary: Lines with higher indentation are called child lines. They can provide values to their parent lines.

Define greeting: binding variables

Let’s greet three times:

display "Hello World!\n"
display "Hello World!\n"
display "Hello World!\n" .
Hello World!
Hello World!
Hello World!

To avoid typing the source text three times, we can define a greeting:

define greeting "Hello World!\n" .

Now greeting is bound to the string "Hello World!\n" and we can use it in place.

define greeting "Hello World!\n" .
display greeting
display greeting
display greeting

define is a special function which creates and replaces the binding to its first argument. Let’s use a different greeting:

define greeting "Hello World!\n" .
define greeting "Hello who?\n" .
display greeting .

Anything which is bound is called a binding. examples of bindings are procedures and variables.

Summary: define creates and replaces bindings.

Hello who: Defining procedures

Let’s make a function ourselves, and call it. We define the procedure hello which greets the name it gets as argument. This will show two important points:

  • Indented lines are calls to functions which are arguments to their parent function, and
  • how to interpret arguments is up to the functions.
define
    hello who
    display "Hello "
    display who
    display "!\n"

hello "World"
Hello World!

As you can see, define acts differently when it receives a function call as first argument: Then it binds a procedure which can be called as function.

You can create procedures with multiple arguments, for example as follows:

define
    greet greeting who
    display greeting
    display " "
    display who
    display "!\n"

greet "Hello" "Wisp" .
Hello Wisp!

The code above also shows why display does not add a newline: That makes it easier to build output incrementally.

Summary: When define receives a function call as first argument, it binds a procedure which can be called.

Use the colon for inline function-calls

Wisp provides a more convenient form of the procedure definition: When you add a colon (:) surrounded by spaces in one line, the rest of the line after the colon gets treated as a function call. This allows making procedure definition more concise:4

define : hello who
    display "Hello "
    display who
    display "!\n"

The indentation of the colon does not matter. If the line with the colon is followed by more deeply indented lines, the call after the colon is treated as indented to the same level. The following four definitions are equivalent:

define : say who
    display who
define
    say who
    display who
define : say who
  display who
define
  say who
  display who

To use a colon as standalone symbol, you must prefix it with a backslash (\:).

Summary: An inline-colon starts a function call as if the rest of the line had been used on its own line, with a deeper indentation of the line which contains the colon.

Hello script: code in files

To avoid typing everyting again and again, you can save your input in a file and call it with wisp. The canonical file extension is .w. So you could save the code above in a file named hello.w. In a terminal, use

guile -x .w --language=wisp hello.w

Hello comments: Communication with humans

A semicolon starts a comment to the end of the line, except if it is in a string or other special form. Example:

define : hello
    display "Hello World!\n"
hello ;; writes a line with Hello World!
Hello World!

Hello whom: conditional execution

Let’s give special treatment to a special someone. We can execute code conditionally.

define : hello who
    cond
        : equal? who "V"
          display "I love you"
        else
          display "Hello "
          display who
    display "!\n"

hello "World"
hello "V" .
Hello World!
I love you!

Each argument of cond is a condition. Conditions are checked one by one. The child-lines of the first condition whose first argument is true are executed and cond stops.

If the line of the condition starts with a colon, the line is called as function, and if it returns true, the condition is treated as true and its child lines executed.

Else is always true.

Using the colon to show an inline function call is similar to the simplification with define, only that for define, the colon just makes the code nicer to read, while here it is actually needed. A line starting with a colon exposes the real raison d'être of the colon: it allows executing children of an empty line. Consequently a colon as the only element in a line denotes the empty line. Using this, we can restate our cond to look more similar to the initial define:

cond
    :
      equal? who "V"
      display "I love you!\n"
    else
      display "..."

This also shows the abstract structure of cond:

cond
    condition1 rest1 ... last1
    condition2 rest2 ... last2
    ...
    else restN ... lastN

The conditions are checked and the first line whose condition is #t is executed, the others are not. The conditions in cond form a tree structure with multiple branches, and when your code runs, cond follows only the first branch whose condition is true.

In the example, condition1 is the child line

equal? who "V"

and there is no rest1, but there is a last1. So it executed the child line

display "I love you!\n"

and returned its result.

More abstractly speaking: cond checks its children and for the first whose first element is true, it executes the rest and returns the last. If these rest and last elements are function calls, they get executed.

Using this, you should know result of the following:

define greeting
    cond
        #f 1 2 "I envie you!"
        #t 3 4 "I love you!"
display greeting .
I love you!

(3 and 4 are seen but are not bound to hello)

Productive intersection: function results

In the previous section we used the function equal?. In the math-section we just used results of calculations without explaining that. With this we skipped how to do this ourself, and that must change: using things which we cannot do ourselves would be unfulfilling.

The simplest case is a function which only does one action, for example double, which doubles its argument:

define : double x
    + x x
display
    double 1
2

Here it is obvious, that double returns the result of its first line. If a function has multiple lines, its result is the result of the last executed line. For example in the following version of double, which displays what it will do before returning the doubled argument.

define : double-loud x
    display "Will double "
    display x
    display "\n"
    + x x
display
    double-loud 1
Will double 1
2

Now, what’s the result of display? Let’s check:

display
    display "The output of display is: "
The output of display is: #<unspecified>

(this also shows nicely that the child lines of procedures are executed before their parent lines: The output from the child display is shown before the output from the parent display)

Summary: A function returns what its last executed line returns.

#t or #f: true or false, a boolean intersection

So what about that equal?? equal? returns either #t or #f, true or false.5 It receives two arguments. If both have equal value, equal? returns #t (true), otherwise it returns #f (false).

But what does it mean to be true?

In wisp — as in its parent Scheme — anything which is not explicitly #f (false) is treated by functions like cond as true.

cond
    #f ;; false
      display "I am never executed"
    0
      display "0: I am executed because I am true and all previous conditions were false"
    ""
      display "\"\": I am true, so I would be executed, if all previous conditions had been false"
    '()
      display "'():  I am true, so I would be executed, if all previous conditions had been false"
    else
      display "else: I am true, so I would be executed, if all previous conditions had been false"
0: I am executed because the previous condition was false

This is much less obvious than the simple statement sounds. Many other languages treat empty lists, or the number zero (0), or the empty string ("") as false. In wisp (as in Scheme), only #f is false.

Summary: anything which is not #f (false) is #t (true).

The un-called line: prefixed by a period

If you played with the above in your mind, you might ask

How do I do that with equal? — how can I have a non-called argument after a function call?

Take as example this:

define who "V"
define hello
    cond
        : equal? who "V"
          ... "I love you!\n"
        ...
display hello .

To allow for this, wisp provides the leading period (.) as the inverse of the colon: A line prefixed with a period is not called but instead continues its parent line. So the code above can be completed as follows:

define who "V"
define hello
    cond
        : equal? who "V"
          . "I love you!\n"
        else
          . "Hello World!\n"
display hello .
I love you!

Consequently, all of the following lines do exactly the same thing (binding who to "V"):

define who "V"
define who
    . "V"
define
    . who "V"
define
    . who
    . "V"

Also the else condition could be pulled into a single line:

else "Hello World!\n"

It is split into two lines to provide more uniformity with the first condition. That eases understanding.

Hello again: looping with named let

When we defined the greeting, we removed duplication by binding the greeting to the variable greeting. But we still wrote display three times:

define greeting "Hello World!\n"
display greeting
display greeting
display greeting .

We can simplify further by making a function hello and moving display and the greeting into it:

define : hello
    display "Hello World!\n"
hello
hello
hello .

Now only one word gets duplicated. But we still write hello three times, and that quickly gets old.

Using a library function,6 we can go down to three lines:

import : srfi srfi-42
do-ec : \: i 3
    display "Hello World!\n"
Hello World!
Hello World!
Hello World!

But let’s take the general path which works in every Scheme, even those not providing SRFI-42.7

The most general and most elegant path to remove the last repetition, is a named let. While a slightly complex tool, it gives you lightsaber-like elegance and expressiveness — while being easier to debug than other looping constructs.

let loop
    : count 3
    cond
      : > count 0
        display "Hello World!\n"
        loop : - count 1
Hello World!
Hello World!
Hello World!

Replacing 5 lines by 6 does not sound like a win, but let-recursion is not limited to just three repetitions: You can simply increase the count with any other number. Also it is the most general construct which can be used to build most control structures.

The name loop is chosen only for convenience and familiarity. You can replace it with any other name.

I’ll take a moment to explain what you see here.

let is a special form with three logical parts: The name, the bindings and the body.

  • Its first part is the name (here loop). It is optional. If you provide it, it allows code within the body of the let to refer to the let as if to a function.
  • Its second part are the bindings. It must exist. If this child line has additional children, each of these defines a variable, like the argument to a function, but with a starting value.
  • Its thirt part is the body (here the cond with its child lines): Its lines are executed. It must have at least one element. The let will return the value of the last executed element in the body. There can be an arbitrary number of lines in the body. Code in the body can use the bindings defined in the bindings. It can also call the name of the let to run through the body again. If you call the let by its name, you must provide one argument per variable in the bindings.

Abstract:

let <name> <header> <body>

Practically this means that you can bind and use more than one variable:

let loop
    : count 3
      worlds 1
    cond
      : > count 0
        cond
          : <= worlds 1
            display "Hello World!\n"
          else
            display "Hello Multiverse!\n"
        loop : - count 1
               + worlds 1
Hello World!
Hello Multiverse!
Hello Multiverse!

Or use the let without name to create helper bindings:

let
  : greeting "Hello Let!\n"
  display greeting
Hello Let!

Inline child-lines and inline infix-math

To conclude the basic structure of wisp, there are but two more concepts: Inline child-lines via round parentheses and infix-math via curly braces.

You can at any point create a child line by inclosing its elements in parentheses:

define (hello) "Hello World!\n"
display (hello)
Hello World!

And you can evaluate math by enclosing it in curly braces like this (mind the spaces around the +, for details see SRFI-105).

display {1 + 2}
3

(there is no operator precedence, though: you have to make it explicit with more braces)

Within child lists enclosed by parentheses or curly braces, you can only create child lists with parentheses or curly braces, because they ignore line breaks.8

Putting these two together allows for a more concise form of the named let:

let loop : (count 3)
    cond
      {count > 0}
          display "Hello World!\n"
          loop {count - 1}

With this, all there is to learn are library APIs and how to build syntax macros, but you can learn all this from the Guile manual.

Summary: inline child-lines via parens and braces allow for more concise code and infix-math.

Let’s finish this basic course with a simple infinite loop (hit CTRL-C to stop it before it arrives):

let infinity :
    infinity

Happy hacking!

Some final points

You can define within a define or a let, but only at the beginning: After the first non-define-line, define is illegal (but let is not, so you can start a let and define within it). Defines can use later defines in the same group. The let makes a new group. Procedures can call all top-level procedures (where the define is not indented) and also themselves and those defines defined within the same group.

When reducing indentation, you must use existing indentation levels (or zero). The following is illegal (even when it works, it can stop working at any time):

define
    something
  here ;; illegal

Also a continuation line (with a period prefix) cannot be followed by a deeper indented line. That would be illogical, because the continuation line counts as part of the parent, so a line without leading period at the same indentation is already a child line of the period-prefixed line.

define
  . something
    here ;; illegal

For more concise code you might want to have a look at when and unless. Further reading: The numerical tower, fast exact math, fibers, guix, records, GOOPS,

To get started playing with your own ideas right away, head over to Starting a Wisp project!

Thank you for reading, and Happy Hacking!


With Guise and Guile

License: cc by-sa

Footnotes:

1

When running Guile from the wisp folder without having wisp installed, add -L .

2

For Schemers: this is either a procedure or a macro.

3

The reason for this behavior is to make wisp in interactive work work exactly like wisp in a text file: you can always copy its code from a file into the interactive wisp shell and it will work. To prevent problems when copying from websites, wisp also allows you to replace every indentation space except for the last one with underscores (see the wisp specification for information).

4

Though conciseness is not the reason why the colon-syntax exists, we’ll come to the real reason, when we play with control structures, specifically cond and let.

5

In the most current Scheme standard (R7RS: Revision 7 of the Scheme Standard),

6

You can make these yourself. Have a look at syntax-case in the Guile manual and at SRFI-42.

7

SRFI’s (spoken like "surfies") are Scheme Requests for Implementation, a community based way to improve Scheme via non-mandatory but recommended features. You won’t find every SRFI in every Scheme, but some like SRFI-1 (lists) are ubiquitous. Wisp itself is SRFI-119.

8

Restricting the creation of further child lists within parens and braces to parens and braces allows using arbitrary regular Scheme code within Wisp. Within braces or parens, it is just Scheme.

Author: Dr. Arne Babenhauserheide

Created: 2021-04-21 Mi 21:35

Emacs 24.5.1 (Org mode 8.2.6)

Validate