ISL+ Recap
The purpose of this page is to provide a brief overview of the ISL+ programming language while assuming familiarity with another programming language. The language this course uses, LSL, is based on ISL+ and consequently requires some knowledge of ISL+.
ISL+ has numbers, strings, and booleans:
> 3 3
> -4 -4
> "Hello!" "Hello!"
> #true #t
> #false #f
Numbers can be written as fractions or decimals:
> 1/2 1/2
> 0.5 0.5
In other programming languages, you’d write arithmetic expressions like 1 + 2 * 3. In ISL+, arithmetic operations are functions: +, -, *, / are all functions.
In other programming languages, you’d call functions like so: add(1, 2, 3). In ISL+, we call functions by surrounding the function and its arguments with parentheses. The first item is the function, and the remaining items are the arguments passed into the function.
> (+ 1 2 3) 6
We’d write the arithmetic expression 1 + 2 * 3 like:
> (+ 1 (* 2 3)) 7
Arguments to functions are evaluated left-to-right. So, the arithmetic expression is evaluated in the following order:
(+ 1 (* 2 3)) ;; 1 evaluates to 1 (+ 1 (* 2 3)) ;; (* 2 3) evaluates to 6 (+ 1 6) ;; (+ 1 6) evaluates to 7 7
Here is how we’d write boolean logic:
> (and #true #true) #t
> (and #true #false) #f
> (and #false #true) #f
> (or #true #true) #t
> (or #true #false) #t
> (or #false false) #f
> (and #true (not #false)) #t
Links to references:
We can define our own constants by the following grammar:
(define FAVORITE-COLOR "pink") (define FAVORITE-NUMBER 5) (define LIKES-PEANUTS? #true)
We write identifiers using kebab-case. Note that it’s customary for top-level constants to be CAPITALIZED, but identifiers can be named in lowercase: favorite-color, favorite-number.
We can define our own functions by the following grammar:
(define (function-name arg1 arg2 arg3) function-body)
For instance, a function that adds the absolute values of its arguments is defined as follows:
(define (add-abs a b) (+ (abs a) (abs b)))
Sometimes it’s necessary to perform actions based on certain conditions. cond exists for this purpose. cond accepts a series of clauses, and each clause has a condition and answer: [condition answer]. cond goes through each clause in order, returning the clause’s answer and stopping if the clause’s condition is satisfied.
(define (drink-temperature drink) (cond [(string=? drink "water") 60] [(string=? drink "matcha tea") 170] [(string=? drink "bubble tea") (+ 40 40)] [(string=? drink "coffee") (* 90 2)]))
In ISL+, a list is a way of creating data with arbitrary size. The list data definition is described recursively: a list is either the empty list '() or the addition of an element onto another list (cons x lst). The following are equivalent:
> (cons 1 (cons 2 (cons 3 '()))) '(1 2 3)
> (list 1 2 3) '(1 2 3)
It’s common enough to perform certain kinds of actions on lists that we have abstractions for them. We pass functions into these abstractions. If we want to do something to each element in a list, we use map. For instance, to add 1 to every element in a list:
> (map add1 (list 1 2 3)) '(2 3 4)
We can also filter our list to keep only elements that satisfy a specific condition – a function that we pass in. That function asks of each element, "should we keep this?" – and so returns a boolean for each element. For instance, if we only wanted to keep the even numbers in a list:
> (filter even? (list 1 2 3)) '(2)
foldr goes through the list and combines the elements together into a single value. We recommend reading How to Design Programs and the documentation for more, but this is used in cases such as when we want to sum the numbers in a list together:
> (foldr + 0 (list 1 2 3)) 6
These list abstractions use functions to perform the computational work – any function works! In the examples above, we used ISL+ functions to do the work for us, but we could also use our own helper functions. Sometimes, our functions are more complex than the ISL+ functions, but also simple enough not to warrant their own function definition. So, we have lambda functions – unnamed functions – which we can pass into these list abstractions. For instance, if we wanted to add 2 to each element in the list, we’d do:
> (map (lambda (num) (+ num 2)) (list 1 2 3)) '(3 4 5)
Observe that the way we define functions is remarkably similar to defining constants that have a function value:
(define (add2 num) (+ num 2))
(define add2 (lambda (num) (+ num 2)))
We also may find it necessary to construct structured data. Here’s how we could create a person:
> (define-struct person [name age]) > (define PERSON-DANIEL (make-person "Daniel" 50)) > (define PERSON-DANIELLA (make-person "Daniella" 20)) > (person-name PERSON-DANIEL) "Daniel"
> (person-age PERSON-DANIELLA) 20
The constructor function make-person, which we can use to make a person such as make-person "Daniel" 50.
The accessor function person-name, which takes in a person that we made and produces the name we gave that person.
The accessor function person-age, which takes in a person that we made and produces the age we gave that person.
In another programming languages, this is like doing person-age(daniel) instead of daniel.age.
Sometimes, we have a super complex function where we’re doing a lot of things. local is a way we help manage that complexity by letting us introduce local definitions instead of top-level ones. We can introduce local variables and local functions, which can only be used inside its body. It has the following structure:
(local [definition1 definition2] local-body)
For example, we could add 3 to each number in a list like so:
> (define (add3-to-list lst) (local [(define (add3 num) (+ num 3))] (map add3 lst))) > (add3-to-list (list 1 2 3)) '(4 5 6)
And add3 only exists inside the local, so we wouldn’t be able to call (add3 0)!