Scheme from the ground up

Text, taken apart

Strings from the inside: cutting them up, joining them, changing their case, and the two conversions that let text join in the arithmetic.

Part 10 of 12

Advanced18 min readBack to Courses

Text is a row of characters

A string is a row of characters with a length, and everything you do to it comes back as another string. string-append joins strings end to end, string-length counts the characters, and string-ref hands you one of them by position.

Positions count from zero, and substring wants two of them: the position to start at and the position to stop before. So (substring "scheme" 0 3) gives the first three characters, and the character at position 3 is not included — a half-open range, which is worth saying out loud once and then trusting.

Text and numbers are different worlds

Numbers that arrive as text cannot take part in arithmetic. string->number turns a string into a number, and number->string turns a number into a string so it can be glued into a message with string-append. Those two are the bridge between the two worlds.

A useful detail: string->number answers #f when the text is not a number, rather than stopping the program. That gives you a test you can use before doing arithmetic on something a reader typed.

Where a string and a list differ

Recursion over characters is easier on a list than on a string, because a list has an empty end that null? can find. string->list converts a string into a list of its characters and list->string puts them back together, so the awkward part is one call at each end.

The chapter example reverses a word this way, and string-upcase does the same job in one step for case. Which to reach for depends on whether the change you want is one of the built-in ones or a rule of your own.

Positions, ranges and characters

Every position in a string is a number, and the first character sits at 0. string-ref gives one character; substring gives a new string made of a range of them.

The second position in substring is exclusive: (substring "scheme" 1 4) starts at the c and stops before the e at position 4, so it answers che. That rule makes the length of the piece the difference between the two positions, with no plus one to remember.

A character is its own type, not a one-letter string. It prints with a hash and a backslash prefix and compares with char=?. Convert it with string when you need to join it to text.

Building text a piece at a time

string-append takes any number of strings and joins them in order. It does not accept numbers, so a count has to cross over with number->string first.

That crossing is the usual shape of a report line: a label, a converted number, and a unit, all glued together. Doing the conversion at the edge keeps the arithmetic in numbers and the output in text.

string->list is the bridge in the other direction. Once the characters are a list, the list functions work on them, and list->string puts the result back together. Reach for it when the rule is yours, and for string-upcase when the built-in already does the job.

Cutting, joining and converting

(define name "Scheme")

(print (string-length name))
(print (substring name 0 3))
(print (string-append name "!"))
(print (string-upcase name))
(print (string-downcase "MAKE IT QUIET"))
(print (string-ref name 1))
(print (string->list name))

; A string is characters, so a recursion can rebuild it.
(define (backwards text)
  (list->string (reverse (string->list text))))

(print (backwards "stressed"))
(print (string->number "42"))
(print (number->string 42))

A string is not a list of characters

They look similar when printed, but they answer different questions. A string has string-length and string-ref; a list has null? and car. Converting with string->list is how you borrow the list vocabulary for a job the string functions do not cover, and list->string is how you hand the result back.

The conversion copies, so it is not free on a long document. For a short word it is invisible; for a file of text, a loop that converts the whole string on every step would be wasteful. Convert once, work on the list, convert back once. The backwards example does exactly that.

Three text mistakes. Positions count from zero, so the last character of a five-letter word sits at 4, not 5. string->number answers #f for text that is not a number, and #f will not add. And string-append will not take a number, even one that looks like text.

Try these next

  • Take the first two characters of three words and join them with string-append.
  • Convert "7" to a number, add 3, and convert the answer back for printing.
  • Reverse a word with string->list, reverse and list->string.

Letters in and letters out

Run it and read the ten lines. Then write a function that takes a word and gives back its first three characters in capitals, and print what it does with "scheme".

Output
Click "Run" to execute your code

Initials and a label

Run it and read the initials and the label. Then write a function that gives the last character of a word, using `string-length` and `substring`.

Output
Click "Run" to execute your code

substring needs both positions, so (substring "hello" 1) is an error in this runtime rather than a convenient shortcut. If you want everything from a position onwards, pass string-length as the second position.

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

What does (substring "scheme" 0 3) give?