My work on this chapter, which looks at how thefunctions
program from Chapter 2 works in the course of discussing input and output.
Exercises
21.1 ❌
The
get-args
procedure has alet
that creates the variablefirst
, and then that variable is used only once inside the body of thelet
. Why doesn’t it just say the following?
1
2
3
4
5
|
(define (get-args n)
(if (= n 0)
'()
(cons (get-arg) (get-args (- n 1)))))
|
Unsure. Procedure seemed to work fine when I tried it with this code. Couldn’t figure out the issue.
Let’s see what Meng Zhang says:
; answer:
; Creating a variable to store the value ofget-arg
inget-args
is necessary because the order to evaluate the subexpressions incons
is unspecified.; If
get-arg
is put intocons
directly and the user’s scheme interpreter evaluates subexpresssions from right to left, (get-args (- n 1)) will be evaluated before (get-arg) incons
. As a result, the first argument the user inputs will be put at the end of the argument list and cause error of “Argument(s) not in domain.”.
Ah interesting. So the fact that there didn’t appear to be anything wrong with me when I was running Scheme was the result of a mere accident of evaluation order of the version of Scheme I am happening to run. But if the evaluation order was the other way around, then the procedure might cons together the arguments 0 and 1 in the opposite order for the /
function and thus lead to a domain issue.
21.2 ✅
The domain-checking function for
equal?
is
1
2
|
(lambda (x y) #t)
|
This seems silly; it’s a function of two arguments that ignores both arguments and always returns
#t
. Since we know ahead of time that the answer is#t
, why won’t it work to haveequal?
‘s entry in the a-list be
1
2
|
(list 'equal? equal? 2 #t)
|
Because the in-domain?
subprocedure wants a procedure it can call on the arguments to check whether they’re in the domain, so the final thing in the a-list for a given entry needs to be a procedure. With the proposed entry above, in-domain
tries to apply #t
to the arguments and this predictably leads to an error…
equal?
has a really broad domain. It can take values like words and lists, booleans, functions, and numbers. So I’m guessing it’s a-list entry is written like this because we’re not really doing a serious domain check here, but we still need some function to plug into the a-list entry given the overall design of the program.
21.3 ✅❌
(Half-credit for only addressing part of the problem)
Every time we want to know something about a function that the user typed in, such as its number of arguments or its domain-checking predicate, we have to do an
assoc
in*the-functions*
. That’s inefficient. Instead, rewrite the program so thatget-fn
returns a function’s entry from the a-list, instead of just its name.
This requires changing the last line of get-fn
to use assoc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
(define (get-fn)
(display "Function: ")
(let ((line (read-line)))
(cond ((empty? line)
(show "Please type a function!")
(get-fn))
((not (= (count line) 1))
(show "You typed more than one thing! Try again.")
(get-fn))
((not (valid-fn-name? (first line)))
(show "Sorry, that's not a function.")
(get-fn))
(else (assoc (first line) *the-functions*)))))
|
Then rename the variable
fn-name
tofn-entry
in thefunctions-loop
procedure,
1
2
3
4
5
6
7
8
9
10
|
(define (functions-loop)
(let ((fn-entry (get-fn)))
(if (equal? (car fn-entry) 'exit)
"Thanks for using FUNCTIONS!"
(let ((args (get-args (arg-count fn-entry))))
(if (not (in-domain? args fn-entry))
(show "Argument(s) not in domain.")
(show-answer (apply (scheme-function fn-entry) args)))
(functions-loop)))))
|
Note that I had to get the car
of fn-entry
to get the name in order for the check for exit
to work properly. I did not do this initially, and it was a source of some confusion later on because exit
was not working correctly and I was unable to discern why!
and rewrite the selectors
scheme-procedure
,arg-count
, and so on, so that they don’t invokeassoc
.
We keep the cadr
and similar invocations in these selectors but refer to the fn-entry
value instead of directly to the association list.
1
2
3
4
5
6
7
8
9
|
(define (scheme-function fn-entry)
(cadr fn-entry))
(define (arg-count fn-entry)
(caddr fn-entry))
(define (type-predicate fn-entry)
(cadddr fn-entry))
|
I also renamed an argument to the in-domain?
procedure. No substantive code changes were required, since in-domain?
relies on type-predicate
to select the appropriate part of the entry from the association list, and type-predicate
was already updated. I just changed the argument name for the sake of clarity of what value was being passed to in-domain?
1
2
3
|
(define (in-domain? args fn-entry)
(apply (type-predicate fn-entry) args))
|
These changes seem consistent with the changes Andrew Buntine made.
I noticed that Meng Zhang made some other changes. I think these are necessary for procedures like every
to work correctly in the functions
program. For example, when trying to invoke every
with first
as the first argument with only the changes I made above, I get the following:
What seems to be happening is that the procedure is trying to get the arg-count
for first
. arg-count
is expecting a list that it can get the caddr
of but is instead given the word first
. At least some of the subprocedures within the functions
program that deal with every
and keep
need to be addressed in a special way, since every
and keep
themselves are special cases that take functions as arguments. Because of that attribute, the changes I made above, which work with most procedures, cause problems for every
and keep
. I’m only giving myself half credit on this since I didn’t spot this issue. Here’s what Meng did:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
(define (named-every fn-name list)
(every (scheme-function (assoc fn-name *the-functions*)) list))
(define (named-keep fn-name list)
(keep (scheme-function (assoc fn-name *the-functions*)) list))
(define (hof-types-ok? fn-name stuff range-predicate)
(let ((fn-entry (assoc fn-name *the-functions*)))
(and (valid-fn-name? fn-name)
(= 1 (arg-count fn-entry))
(word-or-sent? stuff)
(empty? (keep (lambda (element)
(not ((type-predicate fn-entry) element)))
stuff))
(null? (filter (lambda (element)
(not (range-predicate element)))
(map (scheme-function fn-entry)
(every (lambda (x) x) stuff)))))))
|
21.4 ✅
Currently, the program always gives the message “argument(s) not in domain” when you try to apply a function to bad arguments. Modify the program so that each record in
*the-functions*
also contains a specific out-of-domain message like “both arguments must be numbers,” then modifyfunctions
to look up and print this error message along with “argument(s) not in domain.”
Here’s a rewritten in-domain?
which accomplishes this:
1
2
3
4
5
6
7
8
|
(define (in-domain? args fn-entry)
(let ((domain-status (apply (type-predicate fn-entry) args))
(out-of-domain (cadddr (cdr fn-entry))))
(if (not domain-status)
(begin (show-line out-of-domain))
domain-status)
domain-status))
|
Note that show-line
takes a sentence and returns it without parentheses.
Note that I had to use cdr
cuz 4 a/d letters in a row is all the version of Scheme I’m using will recognize for cadr type invocations.
Note that we only want to show the out-of-domain value if the value is actually out of domain, but we also want to return the value domain-status in either case. That’s why there are two calls to domain-status – one for when we show the “side effect” out-of-domain, and one for when we don’t.
I actually decided to revise in-domain?
after realizing that I wanted to use strings and not sentences to represent the out-of-domain messages. show-line
didn’t work for the purpose of returning a string but show
does work.
1
2
3
4
5
6
7
8
|
(define (in-domain? args fn-entry)
(let ((domain-status (apply (type-predicate fn-entry) args))
(out-of-domain (cadddr (cdr fn-entry))))
(if (not domain-status)
(begin (show out-of-domain))
domain-status)
domain-status))
|
I was doubtful whether this exercise would be worth going through initially, but some of the domains were quite tricky to think about and figure out, and it was good to get a different perspective on some of the stuff that we went over in Chapter 2 but with more detail.
Here is my modified list. Note that double blackslashes \\
are used for escaping parentheses inside strings.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
|
(define *the-functions*
(list (list '* * 2 two-numbers? "Both arguments must be numbers")
(list '+ + 2 two-numbers? "Both arguments must be numbers")
(list '- - 2 two-numbers? "Both arguments must be numbers")
(list '/ / 2 can-divide? "The second argument cannot be equal to zero")
(list '< < 2 two-reals? "Both arguments must be real numbers")
(list '<= <= 2 two-reals? "Both arguments must be real numbers")
(list '= = 2 two-numbers? "Both arguments must be numbers")
(list '> > 2 two-reals? "Both arguments must be real numbers")
(list '>= >= 2 two-reals? "Both arguments must be real numbers")
(list 'abs abs 1 real? "The argument must be a real number")
(list 'acos acos 1 trig-range? "The argument must be between 1 and -1")
(list 'and (lambda (x y) (and x y)) 2
(lambda (x y) (and (boolean? x) (boolean? y)))
"Both arguments must be true or false")
(list 'appearances appearances 2 member-types-ok? "The first argument must be a word, and the second argument must either 1\\) be a sentence OR 2\\) be a word IF AND ONLY IF the first argument is a word of exactly 1 character")
(list 'asin asin 1 trig-range? "The argument must be between 1 and -1")
(list 'atan atan 1 number? "The argument must be a number")
(list 'bf bf 1 not-empty? "The argument cannot be empty")
(list 'bl bl 1 not-empty? "The argument cannot be empty")
(list 'butfirst butfirst 1 not-empty? "The argument cannot be empty")
(list 'butlast butlast 1 not-empty? "The argument cannot be empty")
(list 'ceiling ceiling 1 real? "The argument must be a real number")
(list 'cos cos 1 number? "The argument must be a number")
(list 'count count 1 word-or-sent? "The argument must be a word or a sentence")
(list 'equal? equal? 2 (lambda (x y) #t) "This is a placeholder string for a procedure with a very broad domain")
(list 'even? even? 1 integer? "The argument must be an integer")
(list 'every named-every 2
(lambda (fn stuff)
(hof-types-ok? fn stuff word-or-sent?))
"The first argument must be a one argument function and each element of the second argument must be an acceptable argument to the function used as the first argument")
(list 'exit '() 0 '())
; in case user applies number-of-arguments to exit
(list 'exp exp 1 number? "The argument must be a number")
(list 'expt expt 2
(lambda (x y)
(and (number? x) (number? y)
(or (not (real? x)) (>= x 0) (integer? y))))
"Both arguments must be numbers, and AT LEAST one of the following three things must also be true for the arguments to be in the domain of the function:
1\\) the first argument is not a real number (i.e. the argument is an imaginary number), 2\\) the first argument is greater or equal to zero, 3\\) the second argument is an integer")
(list 'first first 1 not-empty? "The argument cannot be empty")
(list 'floor floor 1 real? "The argument must be a real number")
(list 'gcd gcd 2 two-integers? "Both arguments must be integers")
(list 'if (lambda (pred yes no) (if pred yes no)) 3
(lambda (pred yes no) (boolean? pred))
"The first argument must be #t or #f")
(list 'item item 2
(lambda (n stuff)
(and (integer? n) (> n 0)
(word-or-sent? stuff) (<= n (count stuff))))
"The first argument must be a positive integer with a value no greater than the number of characters in the second argument, and the second argument must be a word or sentence")
(list 'keep named-keep 2
(lambda (fn stuff)
(hof-types-ok? fn stuff boolean?))
"The first argument must be a one argument function and each element of the second argument must be an acceptable argument to the function used as the first argument"
)
(list 'last last 1 not-empty? "The argument cannot be empty")
(list 'lcm lcm 2 two-integers? "Both arguments must be integers")
(list 'log log 1 (lambda (x) (and (number? x) (not (= x 0))))
"The argument must be a non-zero number")
(list 'max max 2 two-reals? "Both arguments must be real numbers")
(list 'member? member? 2 member-types-ok? "The first argument must be a word, and the second argument must either 1\\) be a sentence OR 2\\) be a word IF AND ONLY IF the first argument is a word of exactly 1 character")
(list 'min min 2 two-reals? "Both arguments must be real numbers")
(list 'modulo modulo 2 dividable-integers? "Both arguments must be integers and the second argument cannot be zero")
(list 'not not 1 boolean? "The argument must be #t or #f")
(list 'number-of-arguments arg-count 1 valid-fn-name? "The argument must be the name of a valid function")
(list 'odd? odd? 1 integer? "The argument must be an integer")
(list 'or (lambda (x y) (or x y)) 2
(lambda (x y) (and (boolean? x) (boolean? y))) "Both arguments must be #t or #f")
(list 'quotient quotient 2 dividable-integers? "Both arguments must be integers and the second argument cannot be zero")
(list 'random random 1 (lambda (x) (and (integer? x) (> x 0))) "The argument must be a positive integer")
(list 'remainder remainder 2 dividable-integers? "Both arguments must be integers and the second argument cannot be zero")
(list 'round round 1 real? "The argument must be a real number")
(list 'se se 2
(lambda (x y) (and (word-or-sent? x) (word-or-sent? y))) "Both arguments must be words or sentences")
(list 'sentence sentence 2
(lambda (x y) (and (word-or-sent? x) (word-or-sent? y))) "Both arguments must be words or sentences")
(list 'sentence? sentence? 1 (lambda (x) #t) "This is a placeholder string for a procedure with a very broad domain")
(list 'sin sin 1 number? "The argument must be a number")
(list 'sqrt sqrt 1 (lambda (x) (and (real? x) (>= x 0))) "The argument must be a positive real number")
(list 'tan tan 1 number? "The argument must be a number")
(list 'truncate truncate 1 real? "The argument must be a real number")
(list 'vowel? (lambda (x) (member? x '(a e i o u))) 1
(lambda (x) #t) "This is a placeholder string for a procedure with a very broad domain")
(list 'word word 2 (lambda (x y) (and (word? x) (word? y))) "both arguments must be words")
(list 'word? word? 1 (lambda (x) #t) "This is a placeholder string for a procedure with a very broad domain")))
|
Note: I liked how Andrew Buntine handled this – he used an err-message
helper procedure which helped keep the code cleaner.
21.5 ✅❌
(Partial credit for inelegance)
Modify the program so that it prompts for the arguments this way:
but if there’s only one argument, the program shouldn’t sayFirst
:
So I came up with a really ugly and tremendously inelegant solution for this, which I settled on after trying to do more elegant stuff involving procedures that take variable numbers of arguments and getting stuck. Basically, within functions-loop
, I look to see if the arg-count
for fn-entry
is greater than 1. If it is, I go down one branch of the procedure involving two new procedures, get-args-many
and get-arg-many
, that are written to deal with a list of ordinal names, one of which is displayed before Argument: as appropriate.. If arg-count
for fn-entry
is not greater than 1, then the same get-args
and get-arg
procedures that were in use before get used with no list of ordinals, and so Argument: appears as before.
Here is the modified procedure in action:
And here’s the code for the stuff I changed or added. Note that for some reason I did a list of ten ordinal numbers but that was quite unnecessary!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
|
(define (functions-loop)
(let ((fn-entry (get-fn)))
(if (equal? (car fn-entry) 'exit)
"Thanks for using FUNCTIONS!"
(cond
((> (arg-count fn-entry) 1)
(let ((args (get-args-many (arg-count fn-entry) ordinal-list)))
(if (not (in-domain? args fn-entry))
(show "Argument(s) not in domain.")
(show-answer (apply (scheme-function fn-entry) args)))
(functions-loop)))
(else (let ((args (get-args (arg-count fn-entry))))
(if (not (in-domain? args fn-entry))
(show "Argument(s) not in domain.")
(show-answer (apply (scheme-function fn-entry) args)))
(functions-loop)))))))
(define (get-arg-many ordinal)
(display ordinal)
(display " ")
(display "Argument: ")
(let ((line (read-line)))
(cond ((empty? line)
(show "Please type an argument!")
(get-arg-many ordinal))
((and (equal? "(" (first (first line)))
(equal? ")" (last (last line))))
(let ((sent (remove-first-paren (remove-last-paren line))))
(if (any-parens? sent)
(begin
(show "Sentences can't have parentheses inside.")
(get-arg-many ordinal))
(map booleanize sent))))
((any-parens? line)
(show "Bad parentheses")
(get-arg-many ordinal))
((empty? (bf line)) (booleanize (first line)))
(else (show "You typed more than one argument! Try again.")
(get-arg-many ordinal)))))
(define ordinal-list '(First Second Third Fourth Fifth Sixth Seventh Eighth Ninth Tenth))
(define (get-args-many n ordinals)
(if (= n 0)
'()
(let ((first (get-arg-many (car ordinals))))
(cons first (get-args-many (- n 1) (cdr ordinals))))))
|
I thought Meng Zhang’s solution was way more elegant. His get-args
procedure checks if the number of arguments to the procedure is equal to 1. If so, it calls a get-args-single-helper
procedure. Otherwise, it calls a get-args-multi-helper
procedure that takes an extra argument which keeps track of whether the procedure is on the first, second, or third argument. Both these arguments only handle displaying the appropriate argument text/number before recursively calling themselves, and use the existing get-arg
code for other stuff, thus avoiding a bunch of unnecessary duplication that I had.
21.6 ✅
The
assoc
procedure might return#f
instead of an a-list record. How come it’s okay forarg-count
to take thecaddr
ofassoc
‘s return value if(caddr #f)
is an error?
I think what actually happens is that fn-name
invokes get-fn
before we ever get to invoking arg-count
. get-fn
asks the user for the function and uses a procedure called valid-fn-name?
to check whether it’s a valid function. If it’s not a valid function, then the user is prompted for a function again. So by the time arg-count
gets around to invoking caddr
, we’ve already established that a valid function is being sought and thus that assoc
won’t return #f
.
(checked my answer against Meng Zhang’s)
21.7 ✅
Why is the domain-checking predicate for the
word?
function
1
2
|
(lambda (x) #t)
|
instead of the following procedure?
1
2
|
(lambda (x) (word? x))
|
The domain of the function word?
, as distinct from the domain of the function word
, needs to encompass things that are not words for it to be a useful procedure. You don’t want it to be able to only apply to words – you want it to be able to apply to a boolean or a number or a list and say “no, that’s not a word”. So for that reason you want the domain to be very broad. The lambda
that takes a single argument and always returns true is a very broad domain checking predicate.
(checked my answer against Meng Zhang’s)
21.8 ✅
What is the value of the following Scheme expression?
1
2
|
(functions)
|
I think most of the program is side effects. I wonder if the answer is the final thing that gets returned if you successfully exit the program “Thanks for using FUNCTIONS!”. That’s my best guess but I am not sure. The other alternative would be something like “the value depends on the user input”.
Meng Zhang agreed with my best guess.
21.9 ✅
We said in the recursion chapters that every recursive procedure has to have a base case and a recursive case, and that the recursive case has to somehow reduce the size of the problem, getting closer to the base case. How does the recursive call in
get-fn
reduce the size of the problem?
I guess the base case here would be getting a correctly named function. The recursive calls occur when the user has left the line empty, or typed more than one thing, or typed an invalid function. After verifying that one of those conditions has occurred and printing an error message, the program tries a recursive call in order to give the user another opportunity to enter valid input. This isn’t the same thing as just cdr
ing through a list, because the user may or may not enter valid input, but it’s at least potentially involves getting closer to what you could call a base case here.
Meng Zhang agreed.