CS655: Programming Languages, Spring 2001 |
Problem Set 1: Higher-Order Functions - Selected Answers
1. Mergering
a. Write a Scheme procedure listadder that combines two lists by adding their elements. For example, (listadder '(3 1 3) '(3 4 2)) should produce (6 5 5). (Don't worry about making your procedure work for lists of different length.)
Answer: The simplest answer (provided by Michael Deighan, Yuangang Doris Cai and Weilin Zhong):b. Write deeplistadder that works on nested lists also. For example, (deeplistadder '((1 2 (3 4)) 4) '((1 2 (3 4)) 6)) should produce ((2 4 (6 8)) 10).(define (listadder list1 list2) (if (null? list1) '() (cons (+ (car list1) (car list2)) (listadder (cdr list1) (cdr list2)))))
Answer: Doris Cai and Weilin Zhong's answer:c. The deeplistadder procedure could be defined using a more general procedure we can call deeplistmerge. Define deeplistmerge so that deeplistadder could be defined as(define (deeplistadder list1 list2) (if (null? list1) list1 (if (list? (car list1)) (cons (deeplistadder (car list1) (car list2)) (deeplistadder (cdr list1) (cdr list2))) (cons (+ (car list1) (car list2)) (deeplistadder (cdr list1) (cdr list2))))))(define (deeplistadder list1 list2) (deeplistmerge + list1 list2))Answer: Doris Cai and Weilin Zhong's answer:(define (deeplistmerger func list1 list2) (if (null? list1) list1 (if (list? (car list1)) (cons (deeplistmerger func (car list1) (car list2)) (deeplistmerger func (cdr list1) (cdr list2))) (cons (func (car list1) (car list2)) (deeplistmerger func (cdr list1) (cdr list2))))))d. Define a function deeplistmaxer that combined the lists by choosing the higher element. For example, (deeplistmaxer '(3 1 3) '(3 4 2)) should be (3 4 3).
Answer: Dana Wortman and Tom Sabanosh's answer:e. Try to write the deeplistmerge function using some other programming language you know well.(define (deeplistmaxer list1 list2) (deeplistmerger (lambda (x y) (if (> x y) x y)) list1 list2))
(1) For the language you choose, is it possible to write deeplistmerge?(2a) If so, is it harder or easier than it was to do using Scheme? What properties of the languages make it harder or easier?
(2b) If not, is the language you choose a universal programming language? (Reconcile any paradoxes in your answers.)
Answer: No one selected a language in which it is easier to write than in Scheme. One example of a language in which it is easier to write (at least cosmetically) is ML. We will seem more about ML when we cover type inference later in the class. We could define deeplistmerger in ML using:fun deeplistmerge (f, [], []) = [] | deeplistmerge (f, [a]::list1, [b]::list2) = deeplistmerge (f, a, b) @ deeplistmerge(f, list1, list2) | deeplistmerge (f, a::list1, b::list2) = [f(a, b)] @ deeplistmerge(f, list1, list2)Most of you attempted to implement deeplistmerge in C or C++. This required a fair bit of effort and ingenuity. Some things you should have (and most of you did) notice about why this is hard:In Java, it is possible to use the reflection classes (java.lang.reflect) to create a more general deeplistmerge routine (Haiyong Wang and Elisabeth Strunk did this).
- Static type checking - because C is statically typed, it is hard to make a function a flexible as deeplistmerge. Note that the Scheme deeplistmerge works on lists with elements of any type. All of the submitted C and C++ implementations only worked on lists of integers. Using void pointers and other tricks, it is possible to trick C's type checker into letting you write a function that works on any type, but it isn't pretty.
- Functions as first class objects - In Scheme, functions are first class objects which means we can do anything with them we can do with other kinds of objects. In C and C++, functions are not first class objects. We can pass function pointers around, but we cannot create new functions. Further, built in operators are treated specially. So, even though C has a built in + operator, to produce something you could pass as a function pointer, you needed to define a new function.
- Datatypes - Scheme has a built-in list datatype, so it is easy to manipulate lists. C does not, so you either had to write one or invent low level representations. Explicit memory management makes it pretty hard to write a good list datatype in C.
Brian Clarke produced this solution in Perl (excerpted):
@output=&deeplistmerge(sub{$_[0]+$_[1];},$list0,$list1); print "deeplistadder:\n",join(" ",@output),"\n\n"; sub deeplistmerge { ... push(@out,&$f($list0[$i],$list1[$i]));Of course, all of the languages you used are universal, so it must be possible. The question is what it means --- we can produce a function in any universal language that has the same input-output (that is it produces the same result for the same parameters) behavior as any function we can write in Scheme. The easiest way to do this is to write a Scheme interpreter in the other language. Writing a Scheme interpreter in C is pretty easy.Here's Chris Taylor and Mike Tashbook's answer:
While it would be extremely difficult to write a function like deeplistmerge using Java, Java is a universal programming language. Using the proper encoding scheme, any computation is possible. One example of this can be seen in my Master's thesis, where I implemented a Turing Machine simulator using Java; given the proper set of instructions, this Turing Machine simulator can perform any specified computation. My simulator converts these instructions into simpler Java operations, demonstrating that Java can indeed be used to perform any possible computation.2. Whiling Away While
a. I. M. Peirative complains that Scheme can't possibly be a useful language, since it does not have a while loop, and everyone knows you can't write any useful programs without having a while loop.Show I. M. that he is wrong by defining a procedure in scheme that provides the power of a while loop. Using your procedure, I. M. should be able to define factorial using:
(define (factorial n) (while (lambda (x) (<= x n)) ;;; the while predicate (lambda (x) (+ x 1)) ;;; the increment function (lambda (ival accum) (* ival accum)) ;;; the loop body 1 ;;; initial i value 1)) ;;; initial accum valueYour definition should be in the form(define (while pred inc body val accum) ???).Answer: Almost everyone's answer:(define (while pred inc body val accum) (if (pred val) (while pred inc body (inc val) (body val accum)) accum))Another possibility:(define (while pred inc body val accum) (if (pred val) (body val (while pred inc body (inc val) accum)) accum))The first answer is tail recursive. This means, the run-time does not need to build up a stack. The second answer is not tail recursive --- it is (perhaps) more elegant, but requires more space to execute.Alyssa P. Hacker points out that she can use your definition of while to define for so her friend Phor Tran is happy also. She defines for:
(define (for start end body accum) (while (lambda (x) (<= x end)) (lambda (x) (+ x 1)) body start accum))b. Show how factorial can be defined using for.
Answer: Almost everyone got:I. M. is impressed with this, but Pasquale A. Holick claims that while while and for are all good and dandy, but any real programming language has repeat ... until.(define (for start end body accum) (while (lambda (x) (<= x end)) (lambda (x) (+ x 1)) body start accum))c. Show Pasquale that you can provide repeatuntil also. Your definition of repeatuntil should use while, and should not need a recursive call. Pasquale wants to use your definition to define factorial using:
(define (factorial n) (repeatuntil (lambda (x) (> x n)) ;;; the until predicate (stop when true) (lambda (x) (+ x 1)) ;;; the increment function (lambda (ival accum) (* ival accum)) ;;; the loop body 1 ;;; initial i value 1)) ;;; initial accum valueAnswer: All we have to do is negate the predicate:d. Use the power of higher-order functions to invent and define a new control form. Demonstrate that your control form is useful by showing how it can be used to elegantly and concisely express a program.(define (repeatuntil pred inc body val accum) (while (lambda (x) (not (pred x))) inc body val accum))Answer: Most of the new control forms weren't very useful or interesting. There is a good reason for this --- despite 40 years of language design, almost every control form in every language is still nearly identical to something in Algol 60. The only real advances have been in exception handling and method dispatching (which are only arguably control forms).Its not clear if there is some reason why the number of truly useful control forms is so low, or its just that language designers have not been creative enough to come up with good new ones. Common LISP has dozens of control forms (e.g., when, unless, cond, if, case, block, return-from, return, loop, do, do*, dolist, dotimes, for, etc.). This is one of the reasons it is very hard to read and understand someone else's Common LISP program.
Here's Chris Taylor and Mike Tashbook's answer (slightly adapted):
(define (threeway val com bless bequ bgreat) (if (< val com) bless (if (= val com) bequ bgreat))) (define (binary-tree-search val tree) (if (null? tree) (error "Not found") (threeway val (car tree) (tree-search val (car (cdr tree))) val (tree-search val (car (cdr (cdr tree)))))))
University of Virginia Department of Computer Science CS 655: Programming Languages |
David Evans evans@virginia.edu |