|CS 341 - Survey of Programming Languages
Scheme for Java Programmers
|About This Tutorial|
This tutorial is designed for the students of CS 341 at Gettysburg College. Students at this level have expertise in Java programming. Consequently, this tutorial is written for an upper-level Computer Science student with a mature understanding of Java. Since many concepts overlap from one programming language to another, this tutorial will seem accelerated to and be unsuitable for beginning Computer Science students.
It is recommended that the student taking this tutorial first:
Racket is installed on all lab computers. If you own a personal computer, you may also download Racket from the Racket home page.
On our UNIX systems, the Dr. Racket IDE is started with the command "drracket &". (The "&" causes the process to run in parallel so that you regain your terminal prompt.) The Scheme language selection in Racket will be used for this tutorial. Under the "Language" menu, select "Choose Language...", select the "Other Languages" option, and select the Legacy "R5RS" language.
|REPL: Read-Eval-Print Loop|
Scheme's interpreter reads a Scheme expression, evaluates it, prints the result, and repeats the process until there is no more to be interpreted. The "> " prompt indicates that Scheme's interpreter is waiting to read the next expression. Enter the simple expression "123" at this prompt:
Welcome to DrRacket, version 6.3 [3m]. Language: R5RS; memory limit: 128 MB. > 123 123 >
Scheme reads the string expression "123", evaluates it to have the value 123, and prints the string representation of this value "123". A new prompt appears, awaiting the entry of the next expression.
|Operators and Operands|
As with most programming languages, the syntax of Scheme is specified inductively (see text section 1.1.1). That is, we start with base case or terminal symbols (e.g. fundamental data type values, operators, parentheses). Fundamental data type values are our simplest expressions. Additional inductive rules or productions then allow more complex expressions by defining how terminal symbols and expressions may legally be put together.
For example, consider the terminal symbols "(", ")", "+", "1", and "2". The Scheme language is defined such that we can compose an expression from these symbols to add 1 and 2 like this:
> (+ 1 2) 3 >
Try it. The general form for this binary expression is:
( <operator> <operand1> <operand2>)
We will use the terms operator, procedure, and function interchangeably. Also operand and argument are synonymous. If it is helpful, you may think of the previous form as ( <function> <argument1> <argument2>).
More generally, many Scheme expressions take on the form:
(<operator> <operand1> <operand2> ... <operandN>)
These operands may in turn be other arbitrarily complex expressions. That is, expressions may be nested to form more complex expressions:
> (+ 1 (* 2 3)) 7 > (- 4 (/ 10 5)) 2 > (+ (* 1 2) (- (+ 3 4) (/ 6 3))) 7
Expressions with the operator preceding the operands are said to have prefix notation (e.g. (+ 1 2) in Scheme). Expressions with the operator between operands have infix notation (e.g. 1 + 2 of standard mathematics). Expressions with the operator after the operands have postfix notation (e.g. 1 2 + of HP calculators, PostScript and other stack-based languages). Scheme expressions are in prefix notation with surrounding parentheses.
The operator and operands are each evaluated in turn from left to right. To see how Scheme evaluates basic expressions of Scheme, you can use the Stepper, which shows how an expression is evaluated one step at a time. First, enter the expression you'd like to evaluate stepwise in the upper half of the window (the file buffer). Next choose the menu option Language -> Choose Language -> How To Design Programs -> Beginning Student.
When you press the Step button, all of the Scheme expressions in that window will be evaluated stepwise. Try this with the three arithmetic expressions above.
When you're done using the Stepper, change the language back to "R5RS".
|Identifiers, Whitespace, and Comments|
Identifiers are a sequence of letters, digits, and these characters: ! $ % & * + - . / : < = > ? @ ^ _ ~
The sequence of characters cannot begin with a character that could begin a number (e.g. 0-9, -, +, .).
Whitespace (e.g. spaces and newlines) may occur arbitrarily between tokens without changing program meaning. The purpose of whitespace is to make code more readable. For instance, one might format the last Scheme expression above as follows:
> (+ (* 1 2) (- (+ 3 4) (/ 6 3))) 7
Commonly, when lines get too long to follow visually/logically, indentation can greatly clarify the structure. Often, you will use whitespace to vertically align the start of operands.
The semicolon (;) is used to denote a comment. All text from a semicolon to the end of a line is ignored. There are no multiline comments.
There are nine types: boolean, char, number, pair, port, procedure, string, symbol, and vector. We will briefly describe a few types here. Other types merit further description in later sections.
True and false are denoted #t and #f respectively. In Java, we use the infix operator "instanceof" to test class membership. Scheme has prefix operators to test type membership. These are named by following the type name with a question mark. For example:
> (boolean? 3.14) #f > (boolean? #f) #t > (number? #t) #f > (number? 42) #t
Such boolean operators are often called predicates and have names ending with "?" by Scheme programming convention. Other boolean operators include and, or and not:
> (and #t #f) #f > (or #t #f) #t > (and (not #f) (or #t #f)) #t
The number type has several subtypes, each more specific than the previous: number, complex, real, rational, and integer. Each of these subtypes has its own corresponding type membership predicate (e.g. "rational?"). Scheme doesn't prescribe any single internal machine representation of numbers. Rather, it tries to present the programmer with the most abstract and exact numeric programming environment possible. To know whether a number's internal representation is exact or inexact, there are two predicates "exact?" and "inexact?". For example:
> (/ 2 3) 2/3 > (number? (/ 2 3)) #t > (rational? (/ 2 3)) #t > (integer? (/ 2 3)) #f > (exact? (/ 2 3)) #t > (sqrt (/ 2 3)) 0.816496580927726 > (exact? (sqrt (/ 2 3))) #f > (inexact? (sqrt (/ 2 3))) #tOf course, floating point operations like sqrt (square root) will yield inexact values from exact values. Also, operations involving both exact and inexact values will often yield inexact values.
Many useful number functions are described in R5RS section 6.2, including but not limited to: +, -, *, /, quotient, remainder, modulo, max, min, zero?, odd?, even?, positive?, negative?, abs, gcd, lcm, floor, ceiling, round, truncate, exp, log, sin, cos, tan, asin, sqrt, expt, number->string, and string->number.
As in Java, strings are sequences of characters enclosed within double-quotes (") and the backslash (\) acts as an escape character. String indices are zero-based.
Useful string functions are presented in R5RS section 6.3.5, including but not limited to: make-string, string-length, string-ref, string=?, string-ci=? ("ci" = case-insensitive), string<?, substring, string-append, and string-copy.
Characters are written using the notation #\<character> (e.g. #\a, #\B, #\?) or using the notation #\<character name> (e.g. #\space, #\newline).
Useful character functions are presented in R5RS section 6.3.4, including but not limited to: char=?, char-ci=? ("ci" = case-insensitive), char<?, char-whitespace?, char->integer, integer->char, and char-upcase.
> (substring "testing" 0 4) "test" > (substring "testing" 4 7) "ing" > (substring "testing" 4) "ing" > (string-ref "testing" 3) #\t
Symbols are objects that are identical if and only if their names are spelled identically. To quote R5RS: "This is exactly the property needed to represent identifiers in programs, and so most implementations of Scheme use them internally for that purpose." As we shall see, Scheme is a powerful language that can construct new code and immediately execute such code. Thus, the code/data dichotomy of Java doesn't exist in Scheme: Data may in fact be executable code!
Symbols are represented exactly as identifiers. Within code, symbols are preceded by a single-quote (') or appear within a single-quoted data structure. For now, you can think of the single-quote as a way to say to Scheme, "Don't evaluate this (yet)!
> x x: undefined; cannot reference undefined identifier > 'x x > (symbol? 'x) #t
One can also form symbols using the quote operator:
> (quote x) x
|Variables and Assignments|
One may declare global variables (accessible from anywhere in the program) using the define operator. The value of a variable can be changed using the set! operator:
> (define answer 42) > answer 42 > (set! answer (* 6 9)) > answer 54
Believe it or not, you will rarely use these forms. If you find yourself using these a lot, there's a good chance that you should instead be using "let" local variable binding expressions (described later).
|Pairs and Lists|
The pair is the basis of all data structures in Scheme. A pair is constructed by using the cons operator (short for construct) with two operands:
> (cons 'a 'b) (a . b)
From the evaluated result notation, we see why pairs are also sometimes referred to as dotted pairs.
To get the first and second items of a pair, use the operators car and cdr (pronounced could-er), respectively:
> (car (cons 'a (quote b))) a > (cdr '(a . b)) b
The reason for the odd naming of these operators is historic and based in hardware. Steve Russell, implementer of the first Lisp interpreter, relates this sad chapter of programming history:
Such simple operators. Such bizarre names. Feel free to define operators "first" and "rest" to make your code more readable.
Because of an unfortunate temporary lapse of inspiration, we couldn't think of any other names for the 2 pointers in a list node than "address" and "decrement", so we called the functions CAR for "Contents of Address of Register" and CDR for "Contents of Decrement of Register". After several months and giving a few classes in LISP, we realized that "first" and "rest" were better names, and we (John McCarthy, I and some of the rest of the AI Project) tried to get people to use them instead. Alas, it was too late! We couldn't make it stick at all. So we have CAR and CDR.
> (define first car) > (define rest cdr) > (first '(a . b)) a > (rest '(a . b)) b
Lists are constructed inductively in Scheme using cons and a special empty-list symbol '() which is similar Java's null.
Base case: An empty list '() is a list.
> (list? '()) #t
Induction step: A pair constructed of an element and a list is a list.
> (list? (cons 'a '())) #tThat is, add anything to the beginning of a list and it is still a list. This can be repeated arbitrarily, although it's often easy to create lists using the "list" construction operator. Indeed, there are several ways to notate a list.
> (cons 'a (cons 'b (cons 'c '()))) (a b c) > (list 'a 'b 'c) (a b c) > '(a . (b . (c . ()))) (a b c)Note that although lists are built from pairs and the empty-list symbol, list notation is much simplified from the equivalent pair notation form. Understanding how lists are defined, you can now see why "first" and "rest" are appropriate substitutes for "car" and "cdr": "car" gives you the first element of the list, and "cdr" gives you the rest of the list.
> (define first car) > (define rest cdr) > (first '(a b c)) a > (rest '(a b c)) (b c)Recall that we said anything can be added to the beginning of a list. That includes lists.
> (define list-of-lists (list '(a b c) '(d e f) '(g h i))) > (car (car list-of-lists)) a > (car (cdr (car list-of-lists))) bChaining car's and cdr's can get tedious, so Scheme defines a number of abbreviations based on the middle letters of the car/cdr chain (up to 4 long):
> (caar list-of-lists) a > (cadar list-of-lists) b > (caddar list-of-lists) cWhy all of this emphasis on lists? Scheme is a close relative of the language Lisp which is short for LISt Processing. Although Scheme does have a vector data type and loop structures for traversing vectors, it is more natural in this language to store data in lists and process such lists recursively. A programmer who becomes comfortable with Scheme will have a good command of recursive programming skills.
For practice, follow the examples above in extracting all of the letter symbols individually. For more challenging practice, consider the same exercise with a mixture of lists of lists and dotted pairs:
> (define challenge '(((a b) (c . d)) ((e . f) . (g h))))
Imagine being able to create a method that (1) belongs to no class, and (2) can be passed around like data and applied arbitrarily. This is easily done in Scheme. Here is a simple unnamed procedure/function/operator that squares its single argument x:
> (lambda (x) (* x x)) #<procedure>
The procedure can of course be named (although it need not be):
> (define square (lambda (x) (* x x))) > (square 5) 25 > ((lambda (x) (* x x)) 5) 25
The lambda expression has the following form: (lambda <list of identifiers> <body expression>)
> (define add (lambda (x y) (+ x y))) > (add 2 3) 5
When applied to the correct number of operands, the lambda expression created variables with the given identifiers, binds them to the operands, and returns the body expression evaluated in this new variable context. Think of this simply as being like an unnamed method with a list of arguments and a main body that returns its value.
In the above example, the lambda expression creates a new variable x, binds it to the value 5, and evaluates (* x x) in this new context.
Why lambda? This comes from the notation of lambda calculus, a powerful formalism of theoretical computer science. When you really think about it, the lambda procedure is really mind-blowing. It's a procedure to create a procedure. In fact, you can create procedures to create procedures, and so on. The lambda calculus has the power to express all computation. It can compute anything computable by a Turing machine and vice versa. However, the usefulness of the lambda calculus is played out in all functional programming languages.
Consider the following interesting application. There is a handy procedure in Scheme called "map". It takes a procedure and a list as operands, and returns a new list containing the results of the applications:
> (map square '(1 2 3 4 5)) (1 4 9 16 25)
In fact, we could easily express map's functionality using lambda calculus:
> (define my-map (lambda (my-proc my-list) (if (null? my-list) '() (cons (my-proc (car my-list)) (my-map my-proc (cdr my-list)))))) > (my-map square '(1 2 3 4 5)) (1 4 9 16 25)Don't worry if you don't understand this now. We haven't covered "if" yet. We could express "if" in the lambda calculus, but then things would really look horrific. The ability to pass around procedures as data is a very powerful functionality. It also encourages a different functional style of programming which is more focused on the evaluation of expressions than the execution of commands. People with exposure to multiple programming paradigms (e.g object-oriented, functional, etc.), will ultimately become better programmers, able to see elegant solutions that would not occur to a programmer with a limited perspective on programming.
The "apply" operator takes an operator and a list of operands, returning the result of the expression (cons <operator> <operand-list>). Again, notice how the distinction between code and data is blurred:
> (apply + '(1 2 3 4 5)) 15 > (apply square '(5)) 25
If you simply want to evaluate an expression, the operator "eval" followed by the expression and a specified environment will do the trick:
> (eval '(+ 1 2) (interaction-environment)) 3At first, this may seem trivial, but consider that we may build up expressions to be evaluated as part of the program itself. Lists can be Scheme code. Scheme code is composed of lists.
> (eval (cons + (cons 1 (cons 2 '()))) (interaction-environment)) 3In many other non-functional languages, you cannot execute data. Data cannot be procedural in nature. If you can pass procedures, there are often severe limitations. With Scheme, you are free to do really wild programming beyond the scope of this tutorial. Again, don't worry if you cannot appreciate this now. If you're open to seeing things differently, I believe you'll gain a deep appreciation of Scheme's merits with experience. Allow yourself to embrace a different way of thinking!
The if construct is similar to the selection operator in Java, C, and C++. "The what?" you reply. The selection operator. It's often left as a "dusty corner" or "advanced topic" item in teaching these languages. The reason is that it has more of a Scheme/Lisp style than Java/C/C++ programmers are comfortable with. Here's a review of the selection operator:
<condition-expression> ? <true-expression> : <false-expression>
The value of this expression is the value of <true-expression> if the <condition-expression> evaluates to true. Otherwise, this expression has the value of <false-expression>. Here's an example use of the selection operator computing the absolute value of a number:
x = (y >= 0) ? y : -y;This allows one to more succintly express the equivalent form:
if (y >= 0) x = y; else x = -y;
Now we return our attention to Scheme. The "if" expression has the same meaning with the following form:
(if <condition-expression> <true-expression> <false-expression>)
For example, here's how one could create an absolute value procedure using lambda and if.
> (define abs-of-steele (lambda (x) (if (>= x 0) x (- x)))) > (abs-of-steele -1) 1 > (abs-of-steele 1) 1
To understand why this is a horrible pun, click here and here.
For your own exercise, similarly create a procedure "my-max" that takes two number parameters and return the greater one. Test it to see if it works for all cases.
The prefix "and" procedure has similar behavior to the infix "&&" in Java. Java has "lazy" or "short-circuiting" evaluation of &&, in that it stops evaluating subexpressions once it knows the value of the expression. For instance, if we're evaluating (A && B && C && .... && Z) left-to-right in Java and we find that B is false, we don't need any further evaluation to know that the entire expression is false. So we are "lazy" and we "short-circuit" the evaluation to an immediate "false" conclusion.
Scheme operates similarly with one exception: Any value that is not false (#f) is considered true. In the case where "and" does not have an operand evaluate to false, it returns the value of the last non-false expression. For example:
> (and #t #t #t #t) #t > (and #t 3.14 'squirrel) squirrel > (and 'false '() #f 'only-the-#f-is-false) #f > (and 'false '() 'only-the-#f-is-false) only-the-#f-is-false
Although not apparent at first, this allows you to evaluate a series of expressions, with the #f behaving as a simplistic exception signal, with the last expression providing a sort of return value if no such "exception" is encountered.
The Scheme "or" procedure is to Java's "||" as the "and" procedure is to Java's "&&".
> (or #f #f #f #f) #f > (or #f 42 #f) 42 > (or #t #f) #t
The Scheme "cond" (short for conditional) procedure allows a sort of if-else chain case-handling functionality, but is syntactically much more compact. It has the form:
(cond (<condition1> <expression1>) (<condition2> <expression2>) ... (else <expressionN>))
In evaluating the cond expression, Scheme evaluates conditions in order until one has tested non-false or all have tested false. For the first that evaluates to a non-false value, the corresponding expression is evaluated as the value of the entire cond expression. If all conditions test false, then the else expression is evaluated. Here is Horstmann's Richter scale example in Scheme:
(define get-description (lambda (richter) (cond ((>= richter 8.0) "Most structures fall") ((>= richter 7.0) "Many buildings destroyed") ((>= richter 6.0) "Many buildings considerably damaged; some collapse") ((>= richter 4.5) "Damage to poorly constructed buildings") ((>= richter 3.5) "Felt by many people, no destruction") ((>= richter 0.0) "Generally not felt by people") (else "Negative numbers are not valid")))) > (get-description 4.2) "Felt by many people, no destruction" > (get-description 10) "Most structures fall" > (get-description -10000) "Negative numbers are not valid"
The Scheme "case" procedure is similar to but more powerful than the Java/C/C++ switch statement. It is similar in that it chooses from different code alternatives by comparing a value against constants. It is different in that (1) no break statements are need to prevent execution of subsequent cases, (2) one can group sets of related constants, and (3) one can base cases on all types with eqv? equivalence comparison (more on eqv? later). Here are a few examples:
(define get-type (lambda (data) (case data ((0 1 2 3 4 5 6 7 8 9) 'digit) ((#\A #\E #\I #\O #\U #\Y #\a #\e #\i #\o #\u #\y) 'vowel) (else (if (and (char? data) (char-alphabetic? data)) 'consonant 'other))))) > (get-type "Hawaii") other > (get-type 5) digit > (get-type #\o) vowel > (get-type #\s) consonant
As you can see, we compare equivalence of dissimilar types (e.g. strings can be compared to integers). One cannot do so in Java/C/C++ because the language is statically/strongly typed. Scheme is, by contrast dynamically/loosely typed. This means that types of variables are checked at runtime rather than at compile time.
There are interesting tradeoffs for static and dynamic typing. For static typing, you can catch type-mismatch errors at compile time, thus reducing the software testing burden. Additionally, code can run faster if it does not need to dynamically check types at runtime. On the other hand, type errors are not the most common in my experience. One can prototype software more rapidly without the programming burden of declaring and casting types everywhere. Strong typing can also seem overly bureaucratic and restrictive for some tasks.
Consider these tradeoffs carefully when choosing languages. Do you need to optimize runtime speed or speed to market? Does dynamic typing offer benefits for your particular programming task, or is the assurance of a strongly-typed system better for your product's reliability? Dynamically-typed languages are, in my opinion, much more fun to program with. The choice is yours. However, you should withhold judgment until you've gained expertise in both programming worlds.
Just as you will find functional-style constructs in non-functional languages (e.g. the selection operator), you will find constructs that, while not necessary, borrow from other programming styles for convenience. The "begin" operator allows you to list a series of expressions to be evaluated, returning the value of the last one. In this sense, it is much like the "and" operator without false-termination.
Given the surface similarity of a begin expression and a method body with a return statement, one might think one would use begin heavily in Scheme. You might be surprised how little you use begin for our programming purposes. If you try to impose a Java-style to your Scheme programming, you'll likely complicate your task. Pay close attention to the examples we encounter, and try to start from square one stylistically.
> (begin 'task-a 'task-b 'task-c-with-return-value) task-c-with-return-value
doThe "do" operator provides Scheme's very flexible iteration construct. One can define, for any number of variables, their initial values and (optionally) the expressions that update them from one iteration to the next. When the termination condition tests false, the optional iterated expressions are evaluate, otherwise, the return-value is evaluated and becomes the value for the entire expression.
(do ((var1 <initial-value1> [<next-value1>]) (var2 <initial-value2> [<next-value2>]) ... (varN <initial-valueN> [<next-valueN>])) (<termination-condition> <return-value>) [<iterated-expression1>] [<iterated-expression2>] ... [<iterated-expressionN>])
Here's an example of how one can sum a list of numbers:
> (do ((x '(1 2 3 4 5) (cdr x)) (sum 0 (+ sum (car x)))) ((null? x) sum)) 15
However, you will not likely use the loop construct very much. Scheme lends itself both in form and underlying function to the dominant use of recursion. Consider these two ways of writing a procedure to sum a list of numbers:
(define sum-list-do (lambda (lst) (do ((l lst (cdr l)) (sum 0 (+ sum (car l)))) ((null? l) sum)))) (define sum-list (lambda (l) (if (null? l) 0 (+ (car l) (sum-list (cdr l))))))
The first is iterative and makes use of more variables and syntactic components. The second is recursive and would be a more natural implementation choice for many Scheme programmers. Most of our programming tasks lend themselves to elegant recursive solutions, so be skeptical about the "need" for iteration you'll likely assume when starting out.
So far, we've introduced global variables with "define", and local variables with the lambda argument list. We haven't yet shown how to introduce local variables that aren't procedural arguments. There are three forms for doing so: "let", "let*", "letrec".
(let ((<var1> <binding-expression1>) (<var2> <binding-expression2>) ... (<varN> <binding-expressionN>)) <local-expression>)The binding expressions are all evaluated together in the surrounding outer variable context. Then they are assigned in some unspecified order to the local variables. The local expression is then evaluated with respect to the new local variable environment. It's helpful to think of the new local environment being built on top of the outer variable environment. If one of the local variable has the same name as a variable in the surrounding environment, it shadows the outer variable. Shadowing occurs in Java as well. Local variables and parameter variables shadow field variables. In many languages, locality determines scope priority.
Here are some examples:
> (let ((x 2) (y 3)) (+ x y)) 5 > (let ((x 2)) (let ((x 3) (y (+ x 4))) (+ x y))) 9
Look at the latter example more closely. Why isn't the answer 10? If we wish to have the local variables be created and bound in sequence, we use the "let*" operator. "let*" shares the same syntax, but has the meaning of performing a bunch of nested single-binding "let"s.
> (let ((x 2)) (let* ((x 3) (y (+ x 4))) (+ x y))) 10 > (let ((x 2)) (let ((x 3)) (let ((y (+ x 4))) (+ x y)))) 10
"let" is more natural if you're only referring to the outer variable context. "let*" is more natural if there are non-circular dependencies between variables such that they can be bound in sequence. But what if the variables are defined in terms of one another? In Scheme, you can recursively define variables using "letrec". Here's an example (based on a R5RS example) in which two variables holding procedures are defined according to one another:
(letrec ((even? (lambda (n) (or (zero? n) (odd? (- n 1))))) (odd? (lambda (n) (and (not (zero? n)) (even? (- n 1)))))) (even? 42)) #t
In general, prefer the simplest binding construct according to your needs.
There are three general equivalence predicates in Scheme: "eq?", "eqv?", and "equal?". "eq?" is most discriminating and strict in its interpretation of equality. "equal?" is the least discriminating and strict in its interpretation of equality. Detailed description of these can be found in R5RS, but in a nutshell:
> (eqv? 'a 'a) #t > (eqv? '() '()) #t > (eqv? '(a) '(a)) #f > (equal? '(a) '(a)) #t > (equal? '(- a (+ b 1)) '(- a (+ b 1))) #t
You'll generally use "=" for comparing exact numbers, "eqv?" for comparing simple objects, and "equal?" for comparing complex objects.
|More List Operators|
Since list-processing is the forte' of Scheme and Lisp, there are many useful list operators. Here are some basic examples:
> (length '(a b c)) 3 > (append '(a b c) '(d e f)) (a b c d e f) > (reverse '(a b c)) (c b a) > (list-tail '(a b c d e f) 3) (d e f) > (list-ref '(a b c d e f) 3) d > (memv 'c '(a b c d e f)) (c d e f) > (memv 'z '(a b c d e f)) #f > (define assoc-list '((a 1) (b 2) (c 3))) > (assv 'b assoc-list) (b 2) > (assv 'z assoc-list) #f
"list-ref" and "list-tail" can be used to access list elements or tail sublists by index. "memv" can be used to check list membership. "assv" takes a key and an association list (a.k.a. a-list) of key-value pairs and either returns the relevant pair or #f if the key is not present.
As we will be making use of some non-standard input procedures in our programming, we will only cover simple output here.
> (write assoc-list) ((a 1) (b 2) (c 3)) > (write "string") "string" > (display "string") string > (write-char #\newline) > (newline) >
"write" will display any Scheme data in its standard representation form. Thus, a string which is written will be enclosed in double-quotes. The "display" operator will display string in their normal display representation as if each string character were written individually using "write-char". The "newline" operator simply writes a new line to the current output port. As noted earlier, a port is a basic data type. More details on input/output can be found in R5RS section 6.6.
We haven't covered Scheme's library exhaustively (e.g. vectors). Nor have we covered it's semantics or implementation restrictions in rigorous detail. What we have accomplished is a blitz introduction that should give you a good starting foundation for practice. Learning is experiential. Echoing Socrates, Charlie Peacock sang, "We can only possess what we experience." Reading about Scheme is nothing like programming Scheme.
Your real education begins as you approach problems, simple or complex, and translate your solutions into Scheme code. At first, there will be a disconnect between how you think and the how you program Scheme. As all programming languages, Scheme cannot change what it is. However, your mind can change and will learn to translate thoughts into a very different paradigm than it is used to. This stretching exercise of the mind will give you a broader view of programming, a great comfort with recursion, and, if you're open to it, a yearning for functional programming.
© 2017 Todd Neller