UP | HOME

Naming and Logic

programming essentials with Wisp (WIP)

(dark mode)

This article is a start of a text teaching programming basics with Wisp. It is a work in process. The tutorial starts from scratch and teaches in examples with minimal explanation. It’s core is a map of the minimal functionality you need from Scheme to write meaningful programs.

Currently it only contains entries for parts of the map. Each entry in the map will be fleshed out as I get to it. That may take weeks, months, or years, depending on whether and when it grips me.

I’d have loved to publish it only once all is written, but I considered it more important to get this into the open.

I hope that one day it will be interactive, using Guile on wasm (which is currently being built by the folks at Spritely).

To follow along, install Wisp and try the examples as you read.

naming-and-logic-map-of-scheme--sketch.png

PDF (drucken)

The Map of Scheme

Yellow entries are already written and clickable.
naming-and-logic-map-of-scheme.png

naming-and-logic-map-of-scheme.map

(How to make such a map? See Org Mode Tipp: imagemap)

Name a value: define

Use define to name a value. Use . to return a value.

define small-tree-height-meters 3
define large-tree-height-meters 5
. small-tree-height-meters

After typing or copying a block into the Wisp REPL, hit enter three times. You should then see

$1 = 3

This means: the first returned value ($1) is 3. The next time you return a value, it will be called $2.

Names can contain any letter except for (white-)space, quote, comma or parentheses. They must not be numbers.

define illegal name 1
define 'illegal-name 2
define ,illegal-name 3
define illegal)name 4
define 1113841 5
While compiling expression:
Syntax error:
unknown location: source expression failed to match any pattern in form (define illegal name 1)
While reading expression:
#<unknown port>:4:16: unexpected ")"

Use comparisons with numbers

= 3 5
$1 = #f

= 3 3
$1 = #t

#t means true, #f means false.

Returns the result of logic without needing a period (.).

The logic comes first. This is clear for =, but easy to misread for <.

< 3 5 ;; is 3 smaller than 5? #true
< 5 3 ;; is 5 smaller than 3? #false
> 3 5 ;; is 3 bigger than 5? #false
> 5 3 ;; is 5 bigger than 3? #true

> 3 3 ;; is 3 bigger than 3? #false
>= 3 3 ;; is 3 bigger or equal to 3? #true
<= 3 3 ;; is 3 smaller or equal to 3? #true
$1 = #t
$2 = #f
$3 = #f
$4 = #t
$5 = #f
$6 = #t
$7 = #t

Use logic with infix

. {3 = 5}
. {3 < 5}
. {3 > 5}
$1 = #f
$2 = #t
$3 = #f

Infix logic gives a value, so you need . to return it.

Because infix-logic gives a value, you can use it in place of a value.

For example to nest it:

. {{5 < 3} equal? #f}
$1 = #t

Or to name it as value:

define is-math-sane? {3 < 5}
. is-math-sane?
$1 = #t

By convention, names that have the value true or false have the suffix ?.

Use logic with names

define small-tree-height/m 3
define large-tree-height/m 5
. {small-tree-height/m < large-tree-height/m}
$1 = #t

Add comments with ;

define birch-height/m 3
;; this is a comment
define height ;; comment at the end
  ;; comment between lines
  . birch-height/m
. height
$1 = 3

It is common to use ;; instead of ;, but not required. Editors support this style.

Logic with true and false

and #t #t
and #f #t
or #f #t
or #f #f
$1 = #t
$2 = #f
$3 = #t
$4 = #f

If any value passed to and is #f (#false), it ignores further values.
If any value passed to or is not #f (not #false), it ignores further values.

and #t #t #t ;; => #true
and #t #f #t ;; => #false
and {3 < 5} {5 < 3} ;; => #false
or #t #f #t ;; => true
or {3 < 5} {5 < 3} ;; => #true
or #f #f #f ;; => #false
$1 = #t
$2 = #f
$3 = #f
$4 = #t
$5 = #t
$6 = #f

For and and or, everything is #true (#t) except for #false (#f). Given the number of hard to trace errors in other languages that turn up in production, this is the only sane policy.

Name the result of logic with indentation

define birch-h/m 3
define chestnut-h/m 5
define same-héight?
  = birch-h/m chestnut-h/m
define smaller?
  . {birch-h/m < chestnut-h/m}
. smaller?
$1 = #t

The infix directly gives a value, so it needs the . as prefix to return the value instead of trying to call the value as logic.

Name logic with define :

define : same-height? tree-height-a tree-height-b
  = tree-height-a tree-height-b
same-height? 3 3
$1 = #t

By convention, logic that returns true or false has the suffix ?.

You can now use your named logic like all other logic. Even with infix.

define : same-height? tree-height-a tree-height-b
  = tree-height-a tree-height-b
. {3 same-height? 3}
$1 = #t

What this map of Scheme calls named logic is commonly called function or procedure. We’ll stick with logic for the sake of a leaner conceptual mapping.

The indented lines that define the named logic are called the body.

Name a name in define with .

define small-tree-height-meters 3
define height
  . small-tree-height-meters
. height
$1 = 3

. returns the value of its line.

Return the value of logic with .

define : larger-than-4? size
  . {size > 4}
. larger-than-4?
$1 = #<procedure larger-than-4? (size)>

The value of logic defined with define : is a procedure. You can see the arguments in the output: If you call it with too few or too many arguments, you get warnings.

There are other kinds of logic: syntax rules and reader-macros. We will cover syntax rules later. New reader macros are rarely needed; using {...} for infix math is a reader macro.

Name in define : with define

define birch-h/m 3
define : birch-is-small
  define reference-h/m 4
  . { birch-h/m < reference-h/m }
birch-is-small
$1 = #t

Only the last part is returned.

Note the . in front of the { birch-h/m < reference-h/m }: a calculation inside braces is executed in-place, so its value needs to be returned.

Name the result of logic in one line with : or ()

define birch-h/m 3
define chestnut-h/m 5

define same-height : = birch-h/m chestnut-h/m
. same-height
define same-height-again (= birch-h/m chestnut-h/m)
. same-height-again
$1 = #f
$2 = #f

This is consistent with infix-math and uniform with defining logic:

define birch-h/m 3
define chestnut-h/m 5

define same-height {birch-h/m = chestnut-h/m}
. same-height

define : same-height? tree-height-a tree-height-b
  = tree-height-a tree-height-b
define same-height : same-height? birch-h/m chestnut-h/m
. same-height
$1 = #f
$2 = #f

Name text with "

define tree-description "large tree"
define footer "In Love,

Arne"
define greeting
  . "Hello"
display footer
In Love,

Arne

Like { }, text (called string as in “string of characters”) is its value.

Text can span multiple lines. Line breaks in text do not affect code logic.

You can use \n to add a line break within text without having a visual line break. The backslash (\) is the escape character and \n represents a line break. To type a real \ within quotes ( " ), you must escape it as \\.

Return the value with . to name text with indentation.

With display you can show text as it will look in an editor.

Text is stronger than comments:

define with-comment ;; belongs to coment
  ;; comment
  . "Hello ;; part of the text"
. with-comment
$1 = "Hello ;; part of the text"

Take decisions with cond

define chestnut-h/m 5
define tree-description
  cond
    {chestnut-h/m > 4}
      . "large tree"
    : = 4 chestnut-h/m
      . "four meter tree"
    else
      . "small tree"
. tree-description
$1 = "large tree"

cond checks its clauses one by one and uses the first with value #true. To cond every valid value is #true (#t) except for #false (#f).
To use named logic, prefix it with : to check its value.

cond
  5
    . #t
  else ;; else is #true in cond
    . #f
cond
  #f
    . #f
  else
    . #t
$1 = #t
$2 = #t

Use fine-grained numbers with number-literals

define more-precise-height 5.32517
define 100-meters 1e2
. more-precise-height
. 100-meters
$1 = 5.32517
$2 = 100.0

These are floating point numbers. They store approximate values in 64 bit binary, depending on the platform. Read all the details in the Guile Reference manual Real and Rational Numbers, the r5rs numbers, and IEEE 754.

Use exact numbers with #e and quotients

define exactly-1/5 #e0.2
define exactly-1/5-too 1/5
. exactly-1/5
. exactly-1/5-too
$1 = 1/5
$2 = 1/5

Guile computations with exact numbers stay reasonably fast even for unreasonably large or small numbers.

See inexact value of exact number with exact->inexact

exact->inexact #e0.2
exact->inexact 1/5
exact->inexact 2e7
$1 = 0.2
$2 = 0.2
$3 = 2.0e7

The inverse is inexact->exact:

inexact->exact 0.5
$1 = 1/2

Note that a regular 0.2 need not be exactly 1/5, because floating point numbers do not have exact representation for that. You’ll need #e to have precise 0.2.

inexact->exact 0.2
. #e0.2
$1 = 3602879701896397/18014398509481984
$2 = 1/5

Use math with the usual operators as logic

define one-hundred
  * 10 10
define half-hundred : / one-hundred 2
. half-hundred
$1 = 50

Remember that names cannot be valid numbers!

define 100 ;; error!
  * 10 10
While compiling expression:
Syntax error:
unknown location: source expression failed to match any pattern in form (define 100 (* 10 10))

Change the value or logic of a defined name with set!

define birch-h/m 3
set! birch-h/m 3.74
. birch-h/m
$1 = 3.74

It is customary to suffix named logic that changes values of existing names with !.

Since logic can cause changes to names and not just return a result, it is not called function, but procedure in documentation; proc for brevity.

Create nameless logic with lambda

define : is-same-height? a b
  > a b ;; <- this is a mistake
. is-same-height?
is-same-height? 3 3
define : fixed a b
  = a b
set! is-same-height? fixed
. is-same-height? ;; but now called "fixed" in output!
is-same-height? 3 3
;; shorter and avoiding name pollution and confusion.
set! is-same-height?
  lambda : a b
    = a b ;; must be on a new line 
          ;; to not be part of the arguments.
;; since lambda has no name, we see the original name again
. is-same-height?
is-same-height? 3 3
$1 = #<procedure is-same-height? (a b)>
$2 = #f
$3 = #<procedure fixed (a b)>
$4 = #t
$5 = #<procedure is-same-height? (a b)>
$6 = #t

The return value of lambda is logic (a procedure).

Logic knows the name it has been defined as, except if it is defined via lambda.

lambda is a special form. Think of it as define : name arguments, but without the name.

Return list of values with list

list 3 5
define known-heights
  list 3 3.75 5 100
. known-heights
$1 = (3 5)
$2 = (3 3.75 5 100)

You can put values on their own lines by returning their value: . returns all the values in its line. Different from define :, list keeps all values, not just the last.

define known-heights-2
  list 3
     . 3.75 5
     . 100
define known-heights-3
  list
    . 3
    . 3.75
    . 5
    . 100
define : last-height
  . 3 3.75 5 100
= 100 : last-height
$1 = #t

Compare structural values with equal?

= 3 3 3
;; reuse name definition snippets
{{{known-heights}}}
{{{known-heights2}}}
equal? known-heights known-heights-2 known-heights-3
$1 = #t
$2 = (3 5)
$3 = (3 3.75 5 100)
$4 = #t
$5 = #t

Like = and +, equal? can be used on arbitrary numbers of values.

Reusing the snippets here uses the noweb syntax via Emacs Org Mode.

Apply logic to a list of values with apply

apply = : list 3 3
equal? 
  = 3 3
  apply =
    list 3 3
$1 = #t
$2 = #t

Only the last argument of apply is treated as list, so you can give initial arguments:

define a 1
define b 1
apply = a b
  list 1 1
$1 = #t

Get the arguments of named logic as list with . args

define : same? heights
  apply = heights
same? : list 1 1 1
same?
  list 1 1 1
define : same2? . heights
  apply = heights
same2? 1 1 1
same2?
  . 1 1 1
$1 = #t
$2 = #t
$3 = #t
$4 = #t

These are called rest. Getting them is not for efficiency: the list creation is implicit. You can mix regular arguments and rest arguments:

define : same? alice bob . rest
  display : list alice bob rest
  newline
  apply = alice bob rest
same? 1 1 1 1
(1 1 (1 1))
$1 = #t

Remember that apply uses only the last of its arguments as list, in symmetry with . rest.

Get the result of applying logic to each value in lists with map

define birch-h/m 3
define : same-height-as-birch? height/m
  = birch-h/m height/m
define heights : list 3 3.75 5 100
. heights
map same-height-as-birch?
  . heights
map +
  list 1 2 3
  list 3 2 1
map list
  list 1 2 3
  list 3 2 1
$1 = (3 3.75 5 100)
$2 = (#t #f #f #f)
$3 = (4 4 4)
$4 = ((1 3) (2 2) (3 1))

When operating on multiple lists, it takes one argument from each list. All lists must be the same length.

To remember: apply extracts the values from its last argument, map extracts one value from each argument after the first.

Apply logic to each value in lists and ignoring the results with for-each

define birch-h/m 3
define has-birch-height #f
define : set-true-if-birch-height! height/m
  cond
    {birch-h/m = height/m}
      set! has-birch-height #t
define heights : list 3 3.75 5 100
for-each set-true-if-birch-height! heights
. has-birch-height
$1 = #t

Import pre-defined named logic and values with import

import : ice-9 pretty-print
         srfi :1 lists ;; no space after :. it is part of the name

pretty-print
  list 12
    list 34
    . 5 6

first : list 1 2 3 ;; 1
second : list 1 2 3 ;; 2
third : list 1 2 3 ;; 3

member 2 : list 1 2 3 ;; list 2 3 => #true
(12 (34) 5 6)
$1 = 1
$2 = 2
$3 = 3
$4 = (2 3)

Import uses modules which can have multiple components. In the first import, ice-9 is one component and the second is pretty-print. In the second, srfi is the first component, :1 is the second, and lists is the third.

ice-9 is the name for the core extensions of Guile. It’s a play on ice-nine, a fictional perfect seed crystal.

SRFI’s are Scheme Requests For Implementation, portable libraries built in collaboration between different Scheme implementations. The ones available in Guile can be found in the Guile reference manual. More can be found on srfi.schemers.org. They are imported by number (:1) and can have a third component with a name, but that’s not required.

You can use only to import only specific names.

import : only (srfi :1) first second
         only (srfi :1) iota

first : list 1 2 3 ;; 1
second : list 1 2 3 ;; 2
third : list 1 2 3 ;; error
iota 5 ;; list 0 1 2 3 4
$1 = 1
$2 = 2
ice-9/boot-9.scm:1683:22: In procedure raise-exception:
Unbound variable: third

Entering a new prompt.  Type `,bt' for a backtrace or `,q' to continue.
$3 = (0 1 2 3 4)


Apply partial procedures with srfi :26 cut

define : plus-3 number
  + 3 number
map plus-3
  list 1 2 3 ;; list 4 5 6

import : srfi :26 cut

map : cut + 3 <>
  list 1 2 3 ;; list 4 5 6

map : cut - <> 1 ;; => <> - 1
  list 1 2 3 ;; list 0 1 2

map : cut - 1 <> ;; => 1 - <>
  list 1 2 3 ;; list 0 -1 -2

define plus-3-cut : cut + 3 <>
map plus-3-cut
  list 1 2 3 ;; list 4 5 6

$1 = (4 5 6)
$2 = (4 5 6)
$3 = (0 1 2)
$4 = (0 -1 -2)
$5 = (4 5 6)

Use r7rs datatypes, e.g. with vector-map

R7RS is the 7th Revised Report on Scheme. Guile provides a superset of the standard: its core can be imported as scheme base. A foundational datatype is Vectors with O(1) random access guarantee.

import : scheme base
define vec : list->vector '(1 b "third")
vector-map : λ (element) : cons 'el element
           . vec
$1 = #((el . 1) (el . b) (el . "third"))

Vectors have the literal form #(a b c). It is an error to mutate it.

import : scheme base
define mutable-vector : list->vector '(1 b "third")
define literal-vector #(1 b "third")
vector-set! mutable-vector 1 "b" ;; allowed
. mutable-vector
vector-set! literal-vector 1 "b" ;; forbidden
. literal-vector
$1 = #(1 "b" "third")
ice-9/boot-9.scm:1683:22: In procedure raise-exception:
In procedure vector-set!: Wrong type argument in position 1 (expecting mutable vector): #(1 b "third")

Entering a new prompt.  Type `,bt' for a backtrace or `,q' to continue.
$2 = #(1 b "third")


Name structured values with define-record-type

import : srfi :9 records

define-record-type <tree>
  make-tree type height-m weight-kg carbon-kg
  . tree?
  type tree-type
  height-m tree-height
  weight-kg tree-weight
  carbon-kg tree-carbon

define birch-young
  make-tree "birch" 13 90 45 ;; 10 year, 10cm diameter, 
define birch-old
  make-tree "birch" 30 5301 2650 ;; 50 year, 50cm
define birch-weights
  map tree-weight : list birch-young birch-old
. birch-young
. birch-old
. birch-weights
$1 = #<<tree> type: "birch" height-m: 13 weight-kg: 90 carbon-kg: 45>
$2 = #<<tree> type: "birch" height-m: 30 weight-kg: 5301 carbon-kg: 2650>
$3 = (90 5301)

Carbon content in birch trees is about 46% to 50.6% of the mass. See forestry commission technical paper 1993.

Height from Waldwissen, weight from BaumUndErde.

Get the result of logic inline with parentheses (), braces {}, or colon :

import : srfi :26 cut
list 1 2 : + 1 2
   . 4
list 1 2 {1 + 2} 4
list 1 2 (+ 1 2) 4
map (cut + 3 <>) : list 1 2 3
$1 = (1 2 3 4)
$2 = (1 2 3 4)
$3 = (1 2 3 4)
$4 = (4 5 6)

Line breaks and indentation are ignored inside parentheses, except for the value of strings.

The operators that need linebreaks are disabled: colon : and period . do not get the value or return it, but the last value is returned implicitly. This is the default in regular Scheme.

: needs linebreaks, because it only goes to the end of the line.

. needs linebreaks, because it only applies at the beginning of the line (after indentation).

cut is logic that has logic as result.

Handle errors with-exception-handler

;; unhandled exception stops execution
define : add-5 input
  + 5 input ;; illegal for text
map add-5 ' : "five" 6 "seven"
;; check inputs
define : add-5-if input
  if : number? input
     + 5 input
     . #f
map add-5-if ' : "five" 6 "seven"
;; handle exceptions
define : add-5-handler input
  with-exception-handler
    λ (e) : format #t "must be number, is ~S.\n" input
          . #f
    λ () : + 5 input
    . #:unwind? #t ;; #t: continue #f: stop
map add-5-handler ' : "five" 6 "seven"
$1 = #f
ice-9/boot-9.scm:1683:22: In procedure raise-exception:
In procedure +: Wrong type argument in position 1: "five"

Entering a new prompt.  Type `,bt' for a backtrace or `,q' to continue.
$2 = (#f 11 #f)
must be number, is "five".
must be number, is "seven".
$3 = (#f 11 #f)


In Guile Wisp checking inputs is often cheaper than exception handling.

Test your code with srfi 64

Is your code correct?

import : srfi :64 testsuite

define : tree-carbon weight-kg
  * 0.5 weight-kg

define : run-tests
  test-begin "test-tree-carbon"
  test-equal 45.0
    tree-carbon 90
  test-approximate 45.0
    + 40 : random 10.0
    . 5 ;; expected error size
  test-assert : equal? 45.0 : tree-carbon 90
  test-error : throw 'wrong-value
  test-end "test-tree-carbon"

run-tests
%%%% Starting test test-tree-carbon  (Writing full log to "test-tree-carbon.log")
# of expected passes      4
$1 = #<test-runner pass-count: 4 fail-count: 0 xpass-count: 0 xfail-count: 0 skip-count: 0 skip-list: () fail-list: () run-list: #t skip-save: () fail-save: () group-stack: () on-test-begin: #<procedure test-on-test-begin-simple (runner)> on-test-end: #<procedure test-on-test-end-simple (runner)> on-group-begin: #<procedure test-on-group-begin-simple (runner suite-name count)> on-group-end: #<procedure test-on-group-end-simple (runner)> on-final: #<procedure 70e68a95b240 at srfi/srfi-64/testing.scm:285:12 (r)> on-bad-count: #<procedure test-on-bad-count-simple (runner count expected-count)> on-bad-end-name: #<procedure test-on-bad-end-name-simple (runner begin-name end-name)> total-count: 4 count-list: () result-alist: ((source-form test-end "test-tree-carbon") (source-line . 15)) aux-value: #<output: test-tree-carbon.log 13>>

You can use this anywhere.

For details, see srfi 64.

Define derived logic structures with define-syntax-rule

In usual logic application in procedures, arguments are evaluated to their return value first. Procedures evaluate from inside to outside:

import : ice-9 pretty-print
define : hello-printer . args
  pretty-print "Hello"
  for-each pretty-print args
hello-printer 1 : pretty-print "second"
  . 3 4
;; prints "second" "Hello" 1 3 4
"second"
"Hello"
1
#<unspecified>
3
4

But for example cond only evaluates the required branches. It is not a procedure, but a syntax-rule. Syntax-rules evaluate from outside to inside:

import : ice-9 pretty-print
define-syntax-rule : early-printer args ...
  begin
    pretty-print "Hello"
    for-each pretty-print : list args ...
early-printer 1 : pretty-print "second"
  . 3 4
;; prints "Hello" "second" 1 3 4
"Hello"
"second"
1
#<unspecified>
3
4

Arguments of define-syntax-rule are only evaluated when they are passed into a regular procedure or returned. By calling other syntax-rules in syntax-rules, evaluation can be delayed further.

define-syntax-rule can reorder arguments and pass them to procedures and other syntax-rules. It cannot ask for argument values, because it does not evaluate names as values. It operates on names and structure.

Instead of define : name . args, it uses a pattern

define-syntax-rule : name args ...

The ellipsis ... marks args as standing for zero or more names. It must be used with the ellipsis.

The body of define-syntax-rule must only have one element. The logic begin wraps its own body to count as only one element. It returns the value of the last element in its body.

Get and resolve names used in code with quote, eval, and module-ref

list : quote alice
       quote bob
       quote carol
       quote dave
;; => (alice bob carol dave)

define alice "the first"

eval 'alice : current-module
;; => "the first"
module-ref (current-module) 'alice
;; => "the first"
;; module-ref is less powerful than eval. And safer.

define code
  quote
    list 1 2 3
. code 
;; => (list 1 2 3)
;;    uses parentheses form
eval code : current-module
;; => (1 2 3)

' 1 2 3
;; (1 2 3)
list 1 2 3
;; (1 2 3)

equal? : ' 1 2 3
      list 1 2 3
$1 = (alice bob carol dave)
$2 = "the first"
$3 = "the first"
$4 = (list 1 2 3)
$5 = (1 2 3)
$6 = (1 2 3)
$7 = (1 2 3)
$8 = #t

The form ' 1 2 3 is a shorthand to create an immutable (literal) list that is equal? to list 1 2 3.

But some operations like list-set! the-list index new-value from srfi :1 do not work on immutable lists.

define mutable-list : list 1 2 3
list-set! mutable-list 1 'a ;; zero-indexed
. mutable-list
define immutable-list : ' 1 2 3
. immutable-list
list-set! immutable-list 1 'a ;; error!
$1 = a
$2 = (1 a 3)
$3 = (1 2 3)
ice-9/boot-9.scm:1683:22: In procedure raise-exception:
In procedure set-car!: Wrong type argument in position 1 (expecting mutable pair): (2 3)

Entering a new prompt.  Type `,bt' for a backtrace or `,q' to continue.


Build value-lists with quasiquote and unquote

define : tree-manual type height weight carbon-content
  list : cons 'type type
         cons 'height height 
         cons 'weight weight
         cons 'carbon-content carbon-content
tree-manual "birch" 13 90 45

define : tree-quasiquote type height weight carbon-content
  quasiquote
    :
      type . : unquote type
      height . : unquote height
      weight . : unquote weight
      carbon-content . : unquote carbon-content
tree-quasiquote "birch" 13 90 45

define : tree-shorthand type height weight carbon-content
  ` : type . ,type     ;; ` is short for quasiquoted list
      height . ,height ;; , is short for unquote
      weight . ,weight
      carbon-content . ,carbon-content
tree-shorthand "birch" 13 90 45
$1 = ((type . "birch") (height . 13) (weight . 90) (carbon-content . 45))
$2 = ((type . "birch") (height . 13) (weight . 90) (carbon-content . 45))
$3 = ((type . "birch") (height . 13) (weight . 90) (carbon-content . 45))

These three methods are almost equivalent, except that quasiquoting can create an immutable list, but this is not guaranteed.

define three 3
define mutable-list : list 1 2 3
list-set! mutable-list 1 'a ;; zero-indexed
. mutable-list
define immutable-list : ` 1 2 3
list-set! immutable-list 1 'a ;; error!
. immutable-list
define mutable-quasiquoted : ` 1 2 ,three
list-set! mutable-quasiquoted 1 'a ;; currently no error!
. mutable-quasiquoted
$1 = a
$2 = (1 a 3)
ice-9/boot-9.scm:1683:22: In procedure raise-exception:
In procedure set-car!: Wrong type argument in position 1 (expecting mutable pair): (2 3)

Entering a new prompt.  Type `,bt' for a backtrace or `,q' to continue.
$3 = (1 2 3)
$4 = a
$5 = (1 a 3)


Mutating quasiquoted lists may throw an error in the future. From the standard:

A quasiquote expression may return either newly allocated, mutable objects or literal structure for any structure that is constructed at run time …

Reuse your logic with let-recursion

Remember the for-each example:

define has-birch-height #f
define : set-true-if-birch-height! height/m
  define birch-h/m 3
  cond
    {birch-h/m = height/m}
      set! has-birch-height #t
define heights : list 3 3.75 5 100
for-each set-true-if-birch-height! heights
. has-birch-height
$1 = #t

Instead of for-each, we can build our own iteration:

define : has-birch-height? heights
  define birch-h/m 3
  let loop : : heights heights
    cond
      (null? heights) #f
      : = birch-h/m : car heights ;; car is first
        . #t
      else
        loop : cdr heights
define heights : list 3 3.75 5 100
has-birch-height? heights
$1 = #t

null? asks whether the list is empty. car gets the first element of a list, cdr gets the list without its first element.

Recursion is usually easier to debug (all variable elements are available at the top of the let recursion) and often creates cleaner APIs than iteration.

As rule of thumb: start with the recursion end condition (here: (null? heights) and ensure that each branch of the cond either ends recursion or moves a step towards finishing (usually with cdr).

Another example why recursion wins:

define : fib n
   let rek : (i 0) (u 1) (v 1)
        if : >= i : - n 2
           . v
           rek (+ i 1) v (+ u v)

Extend a list with cons

To build your own map function which returns a list of results, you need to add to a list.

cons 1 : list 2 3
;; => list 1 2 3
$1 = (1 2 3)

Used for a simplified map implementation that accepts only a single list:

import : only (srfi :1) first
define : single-map proc elements
  let loop : (changed (list)) (elements elements)
    cond
      : null? elements
        reverse changed
      else
        loop
          ;; add processed first element to changed
          cons : proc : first elements
               . changed
          ;; drop first element from elements
          cdr elements
single-map even? : list 1 2 3
;; => #f #t #f
$1 = (#f #t #f)

Document procedures with docstrings

define : documented-proc arg
  . "Proc is documented"
  . #f ;; doc must not be last element
procedure-documentation documented-proc
;; variables have no docstrings but
;; properties can be set manually.
define variable #f
set-object-property! variable 'documentation
  . "Variable is documented"
object-property variable 'documentation
$1 = "Proc is documented"
$2 = "Variable is documented"
$3 = "Variable is documented"

You can get the documentation with help or ,d on the REPL:

,d documented-proc => Proc is documented
,d variable => Variable is documented

For generating documentation from comments, there’s guild doc-snarf.

;; Proc docs can be snarfed
define : snarfed-proc arg
  . #f
;; Variable docs can be snarfed
define snarfed-variable #f

If this is saved as hello.w, get the docs via

wisp2lisp hello.w > hello.scm && \
    guild doc-snarf --texinfo hello.scm

Read the docs

Now you understand the heart of code. With this as the core there is one more step, the lifeblood of programming: learning more. Sources:

Info manuals can often be read online, but the info commandline application and info in Emacs (C-h i) are far more efficient and provide full-text search. You can use it to read the Guile reference manual and some libraries. Get one by installing texinfo or Emacs.

In interactive wisp (the REPL), you can check documentation:

help string-append .
`string-append' is a procedure in the (guile) module.

- Scheme Procedure: string-append .  args
     Return a newly allocated string whose characters form the
     concatenation of the given strings, ARGS.

,help
Help Commands [abbrev]:
...

Create a manual as package documentation with texinfo

Create a doc/ folder and add a hello.texi file.

An example file can look like the following:

@documentencoding UTF-8
@settitle Hello World
@c This is a comment
@c The Top node is the first page for info
@node Top

@c Show the title
@top
@menu
* First Steps::
* API Reference::
@end menu

@contents

@node First Steps
@chapter First Steps

Quick start: install, minimal example

@itemize
@item
Download from ...
@item
Install: @code{make}.
@end itemize

Example:

@lisp
(+ 1 2)
@result{} 3
@end lisp

@node API Reference
@chapter API Reference

@section Procedures

@subsection hello

Print Hello

@example
hello
@end example

Add a Makefile in the doc/ folder:

all: hello.info hello.epub hello_html/index.html
hello.info: hello.texi
        makeinfo hello.texi
hello.epub: hello.texi
        makeinfo --epub hello.texi
hello_html/index.html: hello.texi
        makeinfo --html hello.texi

Run make:

make

Read the docs with calibre or the browser or plain info:

calibre hello.epub & \
firefox hello_html/index.html & \
info -f ./hello.info

The HTML output is plain. You can adapt it with CSS by adding --css-include=FILENAME or --css-ref=URL.

Alternately you can write an Org Mode document and evaluate (require 'ox-texinfo) to activate exporting to texinfo.

Track changes with a version tracking system like Mercurial or Git

For convenience, first initialize a version tracking repository, for example Mercurial or Git.

# either Mercurial
hg init hello
# or Git
git init hello
# enter the repository folder
cd hello/

Now you can add new files with

# in Mercurial
hg add FILE
# in Git
git add FILE

And take a snapshot of changes with

# in Mercurial
hg commit -m "a change description"
# in Git
git commit -a -m "a change description"

It is good practice to always use a version tracking system.

For additional information and how to publish your code if you want to, see the Mercurial Guide or the Git Tutorial.

Package your code with autoconf and automake

Create a configure.ac file with name, contact info and version.

dnl configure.ac
dnl Name, Version, and contact information.
AC_INIT([hello], [0.0.1], [myName@example.com])
# Find a supported Guile version and set it as @GUILE@
GUILE_PKG([3.0])
GUILE_PROGS
GUILE_SITE_DIR
AC_PREFIX_PROGRAM([guile])
AM_INIT_AUTOMAKE([gnu])
AC_CONFIG_FILES([Makefile])
AC_OUTPUT

Add a Makefile.am with build rules. Only the start needs to be edited:

## Makefile.am rules start
bin_SCRIPTS = hello # program name
SUFFIXES = .w .scm .sh
WISP = hello.w # source files
hello: $(WISP)
        echo "#!/usr/bin/env bash" > "$@" && \
        echo "exec -a $$0 guile --language=wisp -s \"$@\"" \
         >> "$@" && \
        echo ";; exec block finished here: !#" >> "$@" && \
    cat "$^" >> "$@" && chmod +x "$@"
# integrate the texinfo documentation
hello_TEXINFOS = doc/hello.texi
# add library files, prefix nobase_ preserves directories
nobase_site_DATA = 

The rest of the Makefile.am can be copied verbatim.

## Makefile.am technical details

# where to install guile modules to import. See
# https://www.gnu.org/software/automake/manual/html_node/Alternative.html
sitedir = $(datarootdir)/guile/site/$(GUILE_EFFECTIVE_VERSION)

GOBJECTS = $(nobase_site_DATA:%.w=%.go)
nobase_go_DATA = $(GOBJECTS)
godir=$(libdir)/guile/$(GUILE_EFFECTIVE_VERSION)/site-ccachep

# Make sure that the mtime of installed compiled files
# is greater than that of installed source files.  See:
# http://lists.gnu.org/archive/html/guile-devel/2010-07/msg00125.html
# The missing underscore before DATA is intentional.
guile_install_go_files = install-nobase_goDATA
$(guile_install_go_files): install-nobase_siteDATA

EXTRA_DIST = $(WISP)
CLEANFILES = $(GOBJECTS) $(wildcard *~)
DISTCLEANFILES = $(bin_SCRIPTS) $(nobase_site_DATA)

# precompile all source files
.w.go:
        $(GUILE_TOOLS) compile --from=wisp $(GUILE_WARNINGS) \
       -o "$@" "$<"
## Makefile.am help

.PHONY: help
help: ## Show this help message.
        @echo 'Usage:'
        @echo ':make [target] ...' \
        | sed "s/\(target\)/\\x1b[36m\1\\x1b[m/" \
        | column -c2 -t -s :
        @echo
        @echo 'Custom targets:'
        @echo -e "$$(grep -hE '^\S+:.*##' $(MAKEFILE_LIST) \
        | sed -e \
          's/:.*##\s*/:/' -e \
          's/^\(.\+\):\(.*\)/:\\x1b[36m\1\\x1b[m:\2/' \
        | column -c2 -t -s :)"
        @echo
        @echo '(see ./configure --help for setup options)'


This assumes that the folder hello uses a Version tracking system.

## Makefile.am basic additional files
.SECONDARY: ChangeLog AUTHORS
ChangeLog: ## create the ChangeLog from the history
        echo "For user-visible changes, see the NEWS file" > "$@"
        echo >> "$@"
        if test -d "/home/arne/Schreibtisch/arnebab-org/.git"; \
        then cd /home/arne/Schreibtisch/arnebab-org \
        && git log --date-order --date=short \
        | sed -e '/^commit.*$/d' \
        | awk '/^Author/ {sub(/\\$/,""); getline t; print $0 t; next}; 1' \
        | sed -e 's/^Author: //g' \
        | sed -e \
          's/\(.*\)>Date:   \([0-9]*-[0-9]*-[0-9]*\)/\2  \1>/g' \
        | sed -e 's/^\(.*\) \(\)\t\(.*\)/\3    \1    \2/g' \
           >> "$@"; cd -; fi
        if test -d "/home/arne/Schreibtisch/arnebab-org/.hg"; \
        then hg -R "/home/arne/Schreibtisch/arnebab-org" \
           log --style changelog \
           >> "$@"; fi
AUTHORS: ## create the AUTHORS file from the history
        if test -d "/home/arne/Schreibtisch/arnebab-org/.git"; \
          then cd "/home/arne/Schreibtisch/arnebab-org" \
          && git log --format='%aN' \
          | sort -u >> "$@"; cd -; fi
        if test -d "/home/arne/Schreibtisch/arnebab-org/.hg"; \
          then hg -R "/home/arne/Schreibtisch/arnebab-org" \
            --config extensions.churn= \
            churn -t "{author}" >> "$@"; fi

Now create a README and a NEWS file:

#+title: Hello

A simple example project.

* Requirements

- Guile version 3.0.10 or later.

* Build the project

#+begin_src bash
,# Build the project
autoreconf -i && ./configure && make
,# Create a distribution tarball
autoreconf -i && ./configure && make dist
#+end_src

* License

GPLv3 or later.
hello 0.0.1

- initialized the project

And for the sake of this example, a simple hello.w file:

display "Hello World!\n"

To Be Continued …

Also see the Map of R7RS and the Scheme primer.

ArneBab 2023-10-28 Sa 00:00 - Impressum - GPLv3 or later (code), cc by-sa (rest)