Table of Contents

8. Lazy Evaluation

When passing parameters to a function, programming language design offers two options which are not mutually exclusive (both strategies can be implemented in the language):

For more details, see the lecture on lazy evaluation. In Haskell, the default evaluation strategy is lazy.
However, there are ways in which we can force evaluation to be strict. In this lab, we will explore several programming constructs which benefit from lazy evaluation.

8.1. Evaluation

8.1.1. Describe the evaluation strategy of the following expressions:

a.)

foldr ((||).(==1)) False [2,1,3,4,5,6,7]

b.)

foldl (flip ((||).(==1))) False [2,1,3,4,5,6,7]

8.2. Streams

For this lab, streams are synonymous with infinite lists.
If you want to see their content, use the take function (:t take).

8.2.1. Define the stream of natural numbers.

nats :: [Integer]

8.2.2. Using the stream of natural numbers, define the stream of odd numbers and perfect squares. Hint: use higher order functions for a quick solution.

-- 1, 3, 5, 7, ...
odds :: [Integer]
 
-- 0, 1, 4, 9, ...
squares :: [Integer]

8.2.3. Define the stream of Fibonacci numbers. Hint: :t zipWith.

fibs :: [Integer]

8.3. Infinite Binary Trees

Defining a simple binary tree structure in Haskell is easy.
Take this for example:

data BTree = Node Int BTree BTree | Nil deriving Show
 
{-
       1
       |
   ----------
   |        |
   2        3
------    ------ 
|    |    |    |
Nil  Nil  Nil  Nil
-}
 
tree :: BTree
tree = Node 1 (Node 2 Nil Nil) (Node 3 Nil Nil)

But what if we want to enforce an infinite binary tree?
The solution is eliminating the need for the empty node Nil:

-- This is an infinite data type, no way to stop generating the tree
data StreamBTree = StreamNode Int StreamBTree StreamBTree

8.3.1. Define the repeatTree function which takes an Int k and generates an infinite tree, where each node has the value k.

{-
> repeatTree 3
 
             3
             |
         ---------
         |       |
         3       3
      ------   ------   
      |    |   |    |
      3    3   3    3
      .    .   .    .
      .    .   .    .
-}
 
repeatTree :: Int -> StreamBTree

8.3.2. In order to view an infinite tree, we need to convert it to a finite binary tree. Define the function sliceTree, which takes a level k, an infinite tree and returns the first k levels of our tree, in the form of a finite one.

{-
> sliceTree 2 (repeatTree 3)
 
       3
       |
   ----------
   |        |
   3        3
------    ------ 
|    |    |    |
Nil  Nil  Nil  Nil
-}
 
sliceTree :: Int -> StreamBTree -> BTree

8.3.3. Define the generateTree function, which takes a root k, a left generator function leftF and a right generator function rightF.
For example, let's say we have k=2, leftF=(+1), rightF=(*2). This should generate a tree where the root is $ 2$ , the left child is $ parent + 1$
and the right child is $ parent * 2$ .

{-
> generateTree 2 (+1) (*2)
                                    2
                                    |
               -------------------------------------------
               |                                         |            
               3                                         4
        -----------------                         -----------------   
        |               |                         |               |
        4               6                         5               8
    ---------       ---------                 ---------       ---------
    |       |       |       |                 |       |       |       |
    5       8       7       12                6       10      9       16
  -----   -----    -----   -----            -----   -----    -----   ----- 
  |   |   |   |    |   |   |   |            |   |   |   |    |   |   |   |
  6   10  9   16   8   14  13  24           7   12  11  20   10  18  17  32
  .   .   .   .    .   .   .   .            .   .   .   .    .   .   .   .
  .   .   .   .    .   .   .   .            .   .   .   .    .   .   .   .
-}
 
generateTree :: Int -> (Int -> Int) -> (Int -> Int) -> StreamBTree

8.4. Numerical Approximations

8.4.1. Define the build function which takes a generator g and an initial value a0 and generates the stream: [a0, g a0, g (g a0), g (g (g a0)), ... ].

{-
    with this function, you should
    be able to easily define the natural numbers
    with something like: build (+1) 0
-}
 
build :: (Double -> Double) -> Double -> [Double]

8.4.2. Using the build function, define the following streams:

-- 0, 1, 0, 1, 0, 1, 0, ...
alternatingBinary :: [Double]
 
-- 0, -1, 2, -3, 4, -5, ...
alternatingCons :: [Double]
 
-- 1, -2, 4, -8, 16, ...
alternatingPowers :: [Double]

8.4.3. Define the select function which takes a tolerance e and a stream s and returns the nth element of the stream which satisfies the following condition: $ \lvert s_n - s_{n+1} \rvert < e$ .

select :: Double -> [Double] -> Double

Mathematical Constants

8.4.4.

Knowing that $ \displaystyle \lim_{n \rightarrow \infty} \frac{F_{n+1}}{F_n} = \varphi$ , where $ F_n$ is the nth element of the Fibonacci sequence, write an
approximation with tolerance e=0.00001 of the Golden Ration ($ \varphi$ ). Use the previously defined fibs stream and the select function.

phiApprox :: Double

8.4.5.

Consider the sequence:

$ a_{n+1} = a_n + sin(a_n)$ ; where $ a_0$ is an initial approximation, randomly chosen (but not 0 because $ a_{n+1} != a_n$ ).

Knowing that $ \displaystyle \lim_{n \rightarrow \infty} a_n = \pi$ , write an approximation with tolerance e=0.00001 of $ \pi$ . Make sure to use build and select.

piApprox :: Double

Square Root

8.4.6.

Given a number k, we want to create a function which calculates the
square root of k. This is another place where laziness and streams come into play.

Consider the following sequence:

$ a_{n+1} = \frac{1}{2}(a_n + \frac{k}{a_n})$ ; where $ a_0$ is an initial approximation, randomly chosen.

Knowing that $ \displaystyle \lim_{n \rightarrow \infty} a_n = \sqrt{k}$ , write a function that approximates $ \sqrt{k}$ with tolerance e=0.00001. Use build and select.

sqrtApprox :: Double -> Double

Derivatives

8.4.7.

We can approximate the derivative of a function in a certain point using the definition of the derivative:

$ \displaystyle f'(a)=\lim_{h \rightarrow 0} \frac{f(a+h)-f(a)}{h}$

We can obtain better successive approximations of the derivative in a point $ a$ , using a smaller $ h$ .

a) generate the sequence: $ h_0, \frac{h_0}{2}, \frac{h_0}{4}, \frac{h_0}{8}, \ldots$ (where $ h_0$ is a randomly chosen initial approximation)
b) generate the list of approximations for $ f'(a)$ , using the formula above
c) write the function that takes a function $ f$ and a point $ a$ and approximates $ f'(a)$ with tolerance e=0.00001, using the previous steps.

derivativeApprox :: (Double -> Double) -> Double -> Double