4

I have decided to learn some functional language and I hooked up with the lisp-scheme after all.

I am trying to make a function which checks if a list is sorted, either with lowest first getting higher or vice versa, and if it can be sorted it should return true else false.

This is my first code, working only if the list is increasing (or equal).

    (define sorted?
     (lambda (lst)
      (cond ((empty? lst) #t)
            (else (and (<= (car lst) (cadr lst))
                  (sorted? (cdr lst)))))))

clarification: something like (sorted? '(1 2 3 4 5)) and (sorted? '(5 4 3 2 1)) should return true, else if not sorted false of course.

How am I supposed to think when programming in a functional style? The syntax seems straight-forward but I'm not used to the logic.

5 Answers 5

6

Specific implementation

I'd take Óscar López's answer and go one step further:

(define sorted? (lambda (lst)
  (letrec ((sorted-cmp 
            (lambda (lst cmp)
              (cond ((or (empty? lst) (empty? (cdr lst)))
                     #t)
                    (else (and (cmp (car lst) (cadr lst))
                               (sorted-cmp (cdr lst) cmp)))))))
    (or (sorted-cmp lst <=) (sorted-cmp lst >=)))))

The biggest difference between this version and his is that sorted? now defines Óscar's version as an internal helper function using letrec and calls it both ways.

Functional Thinking

You actually chose a good example for illustrating some aspects of how Scheme views the world, and your implementation was off to a very good start.

One important functional principle involved in the solution to this problem is that anything you could put (**here** more stuff '(1 2 3 4)), you can pass around as an argument to another function. That is, functions are first class in a functional programming language. So the fact that you were using <= in your comparison means that you can pass <= as a parameter to another function that makes a comparison accordingly. Óscar's answer is a great illustration of that point.

Another aspect of this problem that embodies another common functional pattern is a function that consists primarily of a (cond) block. In many functional programming languages (Haskell, ML, OCaml, F#, Mathematica), you get stronger pattern matching abilities than you get, by default in Scheme. So with (cond) in Scheme, you have to describe how to test for the pattern that you seek, but that's usually fairly straightforward (for example the (or (empty? lst) (empty? (cdr lst))) in this implementation.

One final functional programming pattern that I see as well-embodied in this problem is that many functional programming solutions are recursive. Recursion is why I had to use letrec instead of plain ol' let.

Almost anything you can do by operating on the first element (or 2 elements as in this case) and then repeating the operation on the tail (cdr) of the list you do that way. Imperative for- or while-style loops aren't impossible in Scheme (although they are pretty much impossible in pure functional languages such as Haskell), they're slightly out of place in Scheme under many circumstances. But Scheme's flexibility that allows you, as the developer, to make that decision enables important performance or maintainability optimizations in certain circumstances.

Continuing exploration

My first implementation of sorted? for my answer here was going to decide which comparison operator to pass to sorted-cmp based on what it saw in the list. I backed off on that when I spotted that a list could start with two equal numbers '(1 1 2 3 4 5). But as I think more about it, there's definitely a way to track whether you've decided a direction yet and, thus, only have one call required to sorted-cmp. You might consider exploring that next.

Sign up to request clarification or add additional context in comments.

4 Comments

thanks for your reply, it was of great help. i think i could have figured out the most of your answercode but the last part, i didnt really know you could recall an argument with an or-clause like that and that it would "remember". because when i saw that code, i thought it would always give true because it checked wether the first two were higher, lower or equal eachother.
also, ive read a bout defining anonymous functions like that, however i think it can be messy using the let, let*, letrec etc and would prefer using (define sorted (lst cmp)) local instead (which worked when i tried), is there a major difference?
"You might consider exploring that next" - OK, I accept your challenge sir, in three languages (see my answer)
@YamanBaron, one reason to use let is that you're only allowed to define a given symbol a single time, whereas let can temporarily override existing defines. In a small program, you can always tell what symbols will already have definitions and you can prove that it's safe to use define. In a slightly bigger program, it's often safest to use let (and its friends let* and letrec) to create appropriately scoped temporary definitions.
5

You almost got it right, look:

(define sorted?
  (lambda (lst)
    (cond ((or (empty? lst) (empty? (cdr lst)))
           #t)
          (else (and (<= (car lst) (cadr lst))
                     (sorted? (cdr lst)))))))

A little modification in the base case, and you're all set. It's necessary to stop when there's only one element left in the list, otherwise the cadr expression will throw an error.

For the second part of your question: If you want to check if it's sorted using a different criterion, simply pass the comparison function as an argument, like this:

(define sorted?
  (lambda (lst cmp)
    (cond ((or (empty? lst) (empty? (cdr lst)))
           #t)
          (else (and (cmp (car lst) (cadr lst))
                     (sorted? (cdr lst) cmp))))))

(sorted? '(1 2 3 4 5) <=)
> #t
(sorted? '(5 4 3 2 1) >=)
> #t

Now if you want to know if a list is sorted in either ascending order or in descending order:

(define lst '(1 2 3 4 5))
(or (sorted? lst >=) (sorted? lst <=))
> #t

As you can see, functional programming is about defining procedures as generic as possible and combining them to solve problems. The fact that you can pass functions around as parameters helps a great deal for implementing generic functions.

1 Comment

thanks for your comment. i was looking specificaly after a way where the cmp-parameter was local defined, but you helped too. thanks.
3

I'm going to take your question to mean, more specifically, "if I already program in an imperative language like C or Java, how do I adjust my thinking for functional programming?" Using your problem as an example, I'm going to spend my Saturday morning answering this question in long form. I'll trace the evolution of a functional programmer through three stages, each a successively higher plane of zen - 1) thinking iteratively; 2) thinking recursively; and 3) thinking lazily.

Part I - Thinking Iteratively

Let's say I'm programming in C and I can't or won't use recursion - perhaps the compiler does not optimize tail recursion, and a recursive solution would overflow the stack. So I start thinking about what state I need to maintain. I imagine a little machine crawling over the input. It remembers if it is searching for an increasing or a decreasing sequence. If it hasn't decided yet, it does so based on the current input, if it can. If it finds input headed in the wrong direction, it terminates with zigzag=true. If it reaches the end of the input, it terminates with zigzag=false.

int
zigzag(int *data, int n)
{
  enum {unknown, increasing, decreasing} direction = unknown;

  int i;
  for (i = 1; i < n; ++i)
    {
      if (data[i] > data[i - 1]) {
    if (direction == decreasing) return 1;
    direction = increasing;
      }
      if (data[i] < data[i - 1]) {
    if (direction == increasing) return 1;
    direction = decreasing;
      }
    }

  /* We've made it through the gauntlet, no zigzagging */
  return 0;
}

This program is typical of C programs: it is efficient but it is difficult to prove that it will do the right thing. Even for this simple example, it's not immediately obvious that this can't get stuck in an infinite loop, or take a wrong turn in its logic somewhere. Of course, it gets worse for more complicated programs.

Part II - Thinking Recursively

I find that the key to writing readable programs in the spirit of functional languages (as opposed to just trying to morph an imperative solution into that language) is to focus on what the program should calculate rather than on how it should do it. If you can do that with enough precision - if you can write the problem out clearly - then most of the time in functional programming, you're almost at the solution!

So let's start by writing out the thing to be calculated in more detail. We want to know if a list zigzags (i.e. decreases at some point, and increases at another). Which lists meet this criterion? Well, a list zigzags if:

  • it is more than two elements long AND
  • it initially increases, but then decreases at some point OR
  • it initially decreases, but then increases at some point OR
  • its tail zigzags.

It's possible to translate the above statements, more or less directly, into a Scheme function:

(define (zigzag xs)
  (and (> (length xs) 2)
       (or (and (initially-increasing xs) (decreases xs))
           (and (initially-decreasing xs) (increases xs))
           (zigzag (cdr xs)))))

Now we need definitions of initially-increasing, initially-decreasing, decreases, and increases. The initially- functions are straightforward enough:

(define (initially-increasing xs)
  (> (cadr xs) (car xs)))

(define (initially-decreasing xs)
  (< (cadr xs) (car xs)))

What about decreases and increases? Well, a sequence decreases if it is of length greater than one, and the first element is greater than the second, or its tail decreases:

(define (decreases xs)
  (letrec ((passes
        (lambda (prev rest)
          (cond ((null? rest) #f)
            ((< (car rest) prev)
             #t)
            (else (passes (car rest) (cdr rest)))))))
    (passes (car xs) (cdr xs))))

We could write a similar increases function, but it's clear that only one change is needed: < must become >. Duplicating so much code should make you uneasy. Couldn't I ask the language to make me a function like decreases, but using > in that place instead? In functional languages, you can do exactly that, because functions can return other functions! So we can write a function that implements: "given a comparison operator, return a function that returns true if that comparison is true for any two successive elements of its argument."

(define (ever op)
 (lambda (xs)
   (letrec ((passes
         (lambda (prev rest)
           (cond ((null? rest) #f)
             ((op (car rest) prev)
              #t)
             (else (passes (car rest) (cdr rest)))))))
     (passes (car xs) (cdr xs)))))

increases and decreases can now both be defined very simply:

(define decreases (ever <))
(define increases (ever >))

No more functions to implement - we're done. The advantage of this version over the C version is clear - it's much easier to reason that this program will do the right thing. Most of this program is quite trivial with all the complexity being pushed into the ever function, which is a quite general operation that would be useful in plenty of other contexts. I am sure by searching one could find a standard (and thus more trustworthy) implementation rather than this custom one.

Though an improvement, this program still isn't perfect. There's lots of custom recursion and it's not obvious at first that all of it is tail recursive (though it is). Also, the program retains faint echos of C in the form of multiple conditional branches and exit points. We can get an even clearer implementation with the help of lazy evaluation, and for that we're going to switch languages.

Part III - Thinking Lazily

Let's go back to the problem definition. It can actually be stated much more simply than it was in part II - "A sequence zigzags (i.e. is non-sorted) if it contains comparisons between adjacent elements that go in both directions". I can translate that sentence, more or less directly, into a line of Haskell:

zigzag xs = LT `elem` comparisons && GT `elem` comparisons

Now I need a way to derive comparisons, the list of comparisons of every member of xs with its successor. This is not hard to do and is perhaps best explained by example.

> xs
[1,1,1,2,3,4,5,3,9,9]

> zip xs (tail xs)
[(1,1),(1,1),(1,2),(2,3),(3,4),(4,5),(5,3),(3,9),(9,9)]

> map (\(x,y) -> compare x y) $ zip xs (tail xs)
[EQ,EQ,LT,LT,LT,LT,GT,LT,EQ]

That's all we need; these two lines are the complete implementation -

zigzag xs = LT `elem` comparisons && GT `elem` comparisons
  where comparisons = map (\(x,y) -> compare x y) $ zip xs (tail xs)

and I'll note that this program makes just one pass through the list to test for both the increasing and decreasing cases.

By now, you have probably thought of an objection: isn't this approach wasteful? Isn't this going to search through the entire input list, when it only has to go as far as the first change of direction? Actually, no, it won't, because of lazy evaluation. In the example above, it calculated the entire comparisons list because it had to in order to print it out. But if it's going to pass the result to zigzag, it will only evaluate the comparisons list far enough to find one instance of GT and one of LT, and no further. To convince yourself of this, consider these cases:

> zigzag $ 2:[1..]
True
> zigzag 1:[9,8..]
True

The input in both cases is an infinite list ([2,1,2,3,4,5..] and [1,9,8,7,6,5...]). Try to print them out, and they will fill up the screen. But pass them to zigzag, and it will return very quickly, as soon as it finds the first change in direction.

A lot of the difficultly in reading code comes from following multiple branches of control flow. And a lot of those branches are really efforts to avoid calculating more than we need to. But much of the same thing can be achieved with lazy evaluation, allowing the program to be both shorter and truer to the original question.

1 Comment

a really good post, i would never think that there would be so many functional programmers really, i thought most went with the c/java and perhaps other fast scripting languages like python. your post was really appreciated and i read it numerous of times to really grasp the content. i have been reading some more indept content regarding scheme, i am for the very moment going through the reputed SICP. about seeing if something is tailrecursive, i have gotten the impression that something is tailrecursive if no operations/calculations is made on the recursive call. 1 can see when using trace.
0

Try this

(define sorted?
  (lambda (l)
     (cond ((null? l) #t)
           (else (check-asc? (car l) (sorted? (cdr l))
                 (check-desc? (car l) (sorted? (cdr l))))))


(define check-asc?
  (lambda (elt lst)
     (cond ((null? lst) #t)
           (else (or (< elt (car lst)) (= elt (car lst))) (check-asc? (car lst) (cdr lst))))))

(define check-desc?
  (lambda (elt lst)
     (cond ((null? lst) #t)
           (else (or (< elt (car lst)) (= elt (car lst))) (check-desc? (car lst) (cdr lst))))))

I am a newbie myself. I haven't tested this code. Still struggling with recursion. Please tell me if it worked or what error it gave.

Comments

0

The previous answer i gave was really bad.

I ran the code in DrScheme and it gave errors.

However I have modified it. Here is a code that works:

(define sorted?
 (lambda (l)
   (cond ((null? l) #t)
       (else (if (check-asc? (car l) (cdr l)) #t
             (check-desc? (car l) (cdr l)))))))


(define check-asc?
(lambda (elt lst)
  (cond ((null? lst) #t)
       (else (if (or (< elt (car lst)) (= elt (car lst))) (check-asc? (car lst) (cdr lst))
                 #f)))))

(define check-desc?
 (lambda (elt lst)
  (cond ((null? lst) #t)
       (else (if (or (> elt (car lst)) (= elt (car lst))) (check-desc? (car lst) (cdr lst))
                 #f)))))

Cases checked:

(sorted? '(5 4 3 2 1)) returns #t

(sorted? '(1 2 3 4 5)) returns #t

(sorted? '(1 2 3 5 4)) returns #f

(sorted? '()) returns #t

(sorted? '(1)) returns #t

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.