[Haskell-beginners] Avoid Case Analyses

Brent Yorgey byorgey at seas.upenn.edu
Sun Oct 7 15:09:02 CEST 2012


Neat, thanks for sharing this, Roger.  For those worried about the
efficiency of 'lower' and 'upper', I suggest as a fun exercise
modifying them to do something like binary search: first jump by
increasing powers of two until the predicate is true, then do a binary
search to find the exact point where the predicate goes from false to
true.

-Brent

On Sun, Oct 07, 2012 at 09:47:32AM +0000, Costello, Roger L. wrote:
> Hi Folks,
> 
> "Programs that avoid case analyses are clearer and simpler than those that use case analyses."
> 
> Richard Bird made that surprising statement in his book, Introduction to Functional Programming using Haskell. 
> 
> It is worthwhile to understand why he said that. In the process we will see a fabulous example of how to start with a specification and systematically develop an implementation.
> 
> An extreme example of case analyses is using it to implement an identity function: 
> 
> color c 	| c == "red"   	= "red"
>              	| c == "green"  	= "green"
>              	| c == "blue"  	= "blue"
> 
> It is read:
> 
> - In the case the color c is "red" then return "red"
> - In the case the color c is "green" then return "green" 
> - In the case the color c is "blue" then return "blue"
> 
> The program is more clearly and simply implemented without case analyses:
> 
> 	color c   =  c
> 
> Let's take a less trivial example. We will implement the floor function. Although Haskell already has a built-in floor function, it will be instructive to see how floor :: Float -> Integer can be programmed. The program will be developed in a systematic manner, starting with a specification for floor. 
> 
> After implementing  floor without case analyses, we will then compare it against an implementation that uses cases analyses. 
> 
> Recall that the floor of x is the largest integer n satisfying n <= x. Here are a couple examples:
> 
> 	floor 2.3      	returns 2
> 	floor (-2.3)  	returns  -3
> 
> Let us begin with a specification of floor:
> 
> 	floor    		::     Float -> Integer
> 	floor x = n 	==   n <= x < n +1
> 
> floor maps a value of type Float to a value of type Integer. The floor of x is n, where x is between n and n +1.
> 
> It is tempting to plunge immediately into a case analysis, considering what to do if x is positive, what to do if x is negative, and, possibly, what to do if it is zero. But the specification does not mention cases and we should try not to mention cases either. 
> 
> How shall we find n, given an arbitrary x? 
> 
> You might respond: Simply truncate x's decimal point and its decimal digits, like so:
> 
> 	truncate 2.3 	returns 2
> 
> But that approach produces the wrong answer for negative values:
> 
> 	truncate (-2.3) 	returns (-2),  whereas the correct answer is -3
> 
> You might then suggest: If x is negative, then truncate and subtract 1.
> 
> But that is descending into a case analysis on the sign of x: if x is positive, then truncate; if x is negative, then truncate and subtract 1.
> 
> We do not need case analyses. The specification suggests an approach. 
> 
> The specification suggests that a search be conducted.
> 
> Search for an n that satisfies the condition n <= x, and then increase n until the condition x < n+1 holds:
> 
> - Start the search from some starting point, say, start n at 0.
> - Lower n until n <= x
> - Increase n until x < n+1
> 
> Example #1: find the floor of (-2.3):
> 
> - Start the search at n = 0
> - Lower n until n <= x:  0, -1, -2, -3
> - Increase n until x < n+1:  this terminates at once since -2.3 < (-3)+1
> 
>   	floor (-2.3) is -3
> 
> Example #2: find the floor of 2.3:
> 
> - Start the search at n = 0
> - Lower n until n <= x:  this terminates at once since 0 < 2.3
> - Increase n until x < n+1:  0, 1, 2
> 
>   	floor (2.3) is 2
> 
> The idea of searching until some condition holds can be encapsulated in a function until, defined by:
> 
> 	until 		::  (a -> Bool) -> (a -> a) -> a -> a
> 	until p f x 	=  if p x then x else until p f (f x)
> 
> Function until takes 3 arguments p, f, and x:
> 
> - p is a Boolean function. Function p is given a value x and it returns True or False. 
> 
> Example: this p returns True if x is less than zero (observe that p is a partially applied function):  
> 
>     	p = (<0)
> 
>    	p (-2)  	returns True
>    	p 2      	returns False
> 
> - f is a function that processes x.
> 
> Example: this f decreases x by one (observe that f is also a partially applied function):
> 
>    	f = (subtract 1)
> 
>    	f 5     	returns 4
>    	f (-5)     returns (-6)
> 
> - x is the value operated on.
> 
> Read this use of function until:
> 
> 	until (<0) (subtract 1) 5 		returns  -1
> 
> like so: starting n at 5, repeatedly subtract 1 until n is less than 0.
> 
> Function until is a built-in Haskell function, as is floor, but for learning purposes we will use our own implementation of them, so place this at the top of your file to hide the built-in version:
> 
> 	import Prelude hiding (until, floor)
> 
> To search for an n satisfying the condition n <= x we will use this function lower:
> 
> 	x = (-2.3)
> 
> 	lower  =  until (<=x) decrease 
>                  	   where decrease n  =  n - 1
> 
> Function lower decreases its argument n until the condition n <= x is satisfied:
> 
> 	lower 5		returns  -3
> 	lower (-5)	returns  -5
> 
> Suppose we wish to find the floor of (5.3) and we start our search at n = 0. Function lower immediately returns because the condition 0 <= 5.3 is immediately satisfied:
> 
> 	x = 5.3
> 	lower 0		returns 0
> 
> In this example, the result n found by the search is too small. We need a second search to increase n until the condition x < n+1 is satisfied. We will increase n in steps of 1 to ensure the condition n <= x is maintained. Actually, we will increase n until it just exceeds x and then decrease it by 1. We create a function upper for this:
> 
> 	upper  =  until (>x) increase
>                                where increase n  =  n + 1
> 
> 	decrease n = n - 1
> 
> The output of function upper will be the input to function decrease, so we compose the two functions:
> 
> 	decrease . upper
> 
> Further, we want the input of (decrease . upper) to be the output of function lower, so we compose them:
> 
> 	decrease . upper . lower
> 
> Example: we wish to find the floor of (5.3) and we start our search at n=0. Function lower immediately returns 0 because the condition 0 <= 5.3 is satisfied. Function upper increases 0 to 6, at which point 6 > 5.3 is satisfied. Finally, 6 is decreased by 1 to the answer 5:
> 
> 	x = 5.3
> 	(decrease . upper . lower) 0	returns 5
> 
> We now have all the ingredients needed to create function floor. The function takes one argument, x:
> 
> 	floor x
> 
> It starts the search from some point, let's choose n=0 as the starting point:
> 
> 	floor x  =  searchFrom 0
> 
> Function searchFrom lowers n=0 until the condition n <= x is satisfied and then raises n until the condition n > x is satisfied and then decreases n by 1: 
> 
> 	floor x  =  searchFrom 0
> 		   where  searchFrom  =  decrease . upper . lower
> 
> Add the definitions for decrease, upper, and lower:
> 
> 	floor x  =  searchFrom 0
>                 where 	searchFrom  	=  decrease . upper . lower
>                             	lower              	=  until (<=x) decrease
>                           	upper               	=  until (>x) increase
>                           	decrease n  	=  n - 1
>                           	increase n  	=  n + 1
> 
> Notice that this implementation of function floor does not use case analyses. The program is surprisingly short, owing mainly to the absence of a case analysis on the sign of x.
> 
> Compare that with a version that uses case analyses: 
> 
> floor x 	| x < 0   	=  lower 0
>             	| x > 0   	=  (decrease . upper) 0
>             	| x == 0  	=  0
>                where  lower       	=  until (<=x) decrease
>                             upper       	=  until (>x) increase
>                             decrease n 	=  n - 1
>                             increase n 	=  n + 1
> 
> Read as: if x is less than 0 then lower 0 until n <= x is satisfied. If x is greater than 0 then increase 0 until n > x is satisfied and then decrease n by 1. Lastly, if x equals 0 then return 0.
> 
> The case analysis version is longer and arguably more complex.
> 
> /Roger
> 
> _______________________________________________
> Beginners mailing list
> Beginners at haskell.org
> http://www.haskell.org/mailman/listinfo/beginners



More information about the Beginners mailing list