Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Both sides previous revision Previous revision
Next revision
Previous revision
pp:2023:haskell:l08 [2023/04/25 19:09]
george.vanuta
pp:2023:haskell:l08 [2023/04/28 17:43] (current)
george.vanuta bolding
Line 5: Line 5:
   * **applicative** (also called **strict**) evaluation strategy:   * **applicative** (also called **strict**) evaluation strategy:
     * parameters are always evaluated **first**     * parameters are always evaluated **first**
-    * can be further refined into: **call-by-value** (e.g. as it happens in C) and **call-by-reference** (e.g. as it happens for objects in Java). +    * can be further refined into: **call-by-value** (e.g. as it happens in //C//) and **call-by-reference** (e.g. as it happens for objects in //Java//). 
-  * **normal** evaluation strategy (also called **non-strict**,​ and when implemented as part of the PL - **call-by-name**)+  * **normal** evaluation strategy (also called **non-strict**,​ and when implemented as part of the //PL// - **call-by-name**)
     * the function is always evaluated **first**     * the function is always evaluated **first**
     * can be further refined into: **lazy**, which ensures that each expression is evaluated **at most once**     * can be further refined into: **lazy**, which ensures that each expression is evaluated **at most once**
  
-For more details, see the lecture on lazy evaluation. In Haskell, the default evaluation strategy is **lazy**.\\+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. 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.
  
Line 16: Line 16:
  
 **8.1.1.** Describe the **evaluation strategy** of the following expressions:​ **8.1.1.** Describe the **evaluation strategy** of the following expressions:​
 +
 +**a.)**
  
 <code haskell> <code haskell>
Line 22: Line 24:
  
 </​code>​ </​code>​
 +
 +**b.)**
  
 <code haskell> <code haskell>
Line 48: Line 52:
 -- 1, 3, 5, 7, ... -- 1, 3, 5, 7, ...
 odds :: [Integer] odds :: [Integer]
 +
 -- 0, 1, 4, 9, ... -- 0, 1, 4, 9, ...
 squares :: [Integer] squares :: [Integer]
Line 60: Line 65:
  
 </​code>​ </​code>​
-===== 8.3. Numerical Approximations ===== 
  
-**8.3.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)), .%%.%%. ]''​.+===== 8.3. Infinite Binary Trees ===== 
 + 
 +Defining a simple **binary tree** structure in //Haskell// is easy.\\ 
 +Take this for example: 
 + 
 +<code haskell>​ 
 + 
 +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) 
 + 
 +</​code>​ 
 + 
 +But what if we want to enforce an **infinite binary tree**?\\ 
 +The solution is eliminating the need for the empty node ''​Nil'':​ 
 + 
 +<code haskell>​ 
 + 
 +-- This is an infinite data type, no way to stop generating the tree 
 +data StreamBTree = StreamNode Int StreamBTree StreamBTree 
 + 
 +</​code>​ 
 + 
 +**8.3.1.** Define the ''​repeatTree''​ function which takes an **Int** ''​k''​ and generates an **infinite tree**, where each node has the value ''​k''​. 
 + 
 +<code haskell>​ 
 + 
 +{- 
 +> repeatTree 3 
 + 
 +             3 
 +             | 
 +         ​--------- 
 +         ​| ​      | 
 +         ​3 ​      3 
 +      ------ ​  ​------ ​   
 +      |    |   ​| ​   | 
 +      3    3   ​3 ​   3 
 +      .    .   ​. ​   . 
 +      .    .   ​. ​   . 
 +-} 
 + 
 +repeatTree :: Int -> StreamBTree 
 + 
 +</​code>​ 
 + 
 +**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. 
 + 
 +<code haskell>​ 
 + 
 +{- 
 +> sliceTree 2 (repeatTree 3) 
 + 
 +       3 
 +       | 
 +   ​---------- 
 +   ​| ​       | 
 +   ​3 ​       3 
 +------ ​   ------  
 +|    |    |    | 
 +Nil  Nil  Nil  Nil 
 +-} 
 + 
 +sliceTree :: Int -> StreamBTree -> BTree 
 + 
 +</​code>​ 
 + 
 +**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 $math[2], the //left child// is $math[parent + 1]\\ 
 +and the //right child// is $math[parent * 2]. 
 + 
 +<code haskell>​ 
 + 
 +{- 
 +> 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 
 + 
 +</​code>​ 
 + 
 +===== 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)), .%%.%%. ]''​.
  
 <code haskell> <code haskell>
  
 {- {-
-    ​having ​this function, you should +    ​with this function, you should 
-    ​easily ​be able to define the natural numbers+    be able to easily ​define the natural numbers
     with something like: build (+1) 0     with something like: build (+1) 0
 -} -}
Line 76: Line 191:
 </​code>​ </​code>​
  
-**8.3.2.** Using the ''​build''​ function, define the following **streams**:​+**8.4.2.** Using the ''​build''​ function, define the following **streams**:​
  
 <code haskell> <code haskell>
Line 86: Line 201:
 alternatingCons :: [Double] alternatingCons :: [Double]
  
--- -1, 2, -4, 8, -16, ...+-- 1, -2, 4, -8, 16, ...
 alternatingPowers :: [Double] alternatingPowers :: [Double]
  
 </​code>​ </​code>​
  
-**8.3.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: ​ $math[\lvert s_n - s_{n+1} \rvert < e].+**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: ​ $math[\lvert s_n - s_{n+1} \rvert < e].
  
 <code haskell> <code haskell>
Line 101: Line 216:
 ==== Mathematical Constants ==== ==== Mathematical Constants ====
  
-**8.3.4.** +**8.4.4.** 
  
 Knowing that $math[\displaystyle \lim_{n \rightarrow \infty} \frac{F_{n+1}}{F_n} = \varphi], where $math[F_n] is the nth element of the **Fibonacci** sequence, write an\\ Knowing that $math[\displaystyle \lim_{n \rightarrow \infty} \frac{F_{n+1}}{F_n} = \varphi], where $math[F_n] is the nth element of the **Fibonacci** sequence, write an\\
Line 112: Line 227:
 </​code>​ </​code>​
  
-**8.3.5.** +**8.4.5.** 
  
 Consider the //​sequence//:​ Consider the //​sequence//:​
Line 128: Line 243:
 ==== Square Root ==== ==== Square Root ====
  
-**8.3.6.** +**8.4.6.** 
  
 Given a number ''​k'',​ we want to create a function which calculates the\\ Given a number ''​k'',​ we want to create a function which calculates the\\
Line 147: Line 262:
 ==== Derivatives ==== ==== Derivatives ====
  
-**8.3.7.** +**8.4.7.** 
  
 We can approximate the derivative of a function in a certain point using the definition of the derivative: We can approximate the derivative of a function in a certain point using the definition of the derivative:
Line 155: Line 270:
 We can obtain better successive approximations of the derivative in a point $math[a], using a smaller $math[h]. We can obtain better successive approximations of the derivative in a point $math[a], using a smaller $math[h].
  
-a) generate the sequence: $math[h_0, \frac{h_0}{2},​ \frac{h_0}{4},​ \frac{h_0}{8},​ ...] (where $math[h_0] is a randomly chosen //initial approximation//​)\\ +**a)** generate the sequence: $math[h_0, \frac{h_0}{2},​ \frac{h_0}{4},​ \frac{h_0}{8},​ ...] (where $math[h_0] is a randomly chosen //initial approximation//​)\\ 
-b) generate the list of approximations for $math[f'​(a)],​ using the formula above\\ +**b)** generate the list of approximations for $math[f'​(a)],​ using the formula above\\ 
-c) write the function that takes a function $math[f] and a point $math[a] and approximates $math[f'​(a)] with tolerance ''​e=0.00001'',​ using the previous steps.+**c)** write the function that takes a function $math[f] and a point $math[a] and approximates $math[f'​(a)] with tolerance ''​e=0.00001'',​ using the previous steps.
  
 <code haskell> <code haskell>
Line 165: Line 280:
 </​code>​ </​code>​
  
-==== The Newton-Raphson Method ​====+===== Recommended Reading =====
  
-**8.3.8.**  +  ​* [[http://worrydream.com/refs/Hughes-WhyFunctionalProgrammingMatters.pdf| Why Functional Programming Matters ​(especially section 4 "​Gluing Programs Together"​, where the lab exercises are inspired from)]]
- +
-The method used for approximating the square root is derived from a more general one, named **The Newton-Raphson Method**. +
- +
-This is a generic method used for finding the **roots** of a function ($math[x] | $math[f(x) = 0]). +
- +
-Considering the following //​sequence//​: +
- +
-$math[x_{n+1} = x_n - \frac{f(x_n)}{f'​(x_n)}] +
- +
-and the //limit//+
- +
-$math[\displaystyle \lim_{n \rightarrow \infty} x_n = r\ \Rightarrow \ f(r) = 0] +
- +
-write a function which takes another ''​function''​ and it approximates a root with **tolerance** ''​e=0.00001''​. Make use of ''​build'',​ ''​select''​ and ''​derivativeApprox''​. +
- +
-<code haskell>​ +
- +
-rootApprox :: (Double ​-> Double) -> Double +
- +
-</​code>​ +
- +
-==== Integrals (: ==== +
- +
-**8.3.9.**  +
- +
-Given a function $math[f], we can approximate the definite integral on $ [a, b]$, using the area of the trapezoid defined by $math[a, b, f(a)f(b)]: +
- +
-$math[\displaystyle \int_{a}^{b} f(x) dx \approx (b - a)\frac{f(a)+f(b)}{2}] +
- +
-We can obtain a better approximation by dividing the interval in two and adding the area of the two trapezoids defined by $math[a, m, f(a), f(m)] and $math[m, b, f(m), f(b)] (where $math[m] is the middle of the interval $ [a, b]$). We can obtain a better approximation by dividing these intervals in two and so on. +
- +
-Write a function which approximates the integral of a function on an interval. Follow the steps: +
- +
-a) Write a function which takes a function $math[fand two points $math[a, band calculates the area of the trapezoid $math[a, b, f(a), f(b)]\\ +
- +
-<code haskell>​ +
- +
-trapezoidArea :: (Double -> Double) -> Double -> Double -> Double +
- +
-</​code>​ +
- +
-b) Write a function which takes a (ascending) list of points and inserts between any two points their middle: +
- +
-<code haskell>​ +
- +
--- [1, 4, 7, 10, 13] ->[1, 2.5, 4, 5.5, 7, 8.5, 10, 11.5, 13] +
-insertMiddle :: [Double] -> [Double] +
- +
-</​code>​ +
- +
-c) Write a function which takes a function $math[f] and a list of points $math[p_0,\ p_1,\ p_2,\ p_3,\ ...] and returns the list containing the areas of trapezoids defined by two consecutive points:\\ +
- +
-<code haskell>​ +
- +
--- f -> [p0, p1, p2, p3, ...] -> [area p0 p1 f(p0) f(p1), area p1 p2 f(p1) f(p2), area p2 p3 f(p2) f(p3)] +
-areaConsecutive :: (Double -> Double) -> [Double] -> [Double] +
- +
-</​code>​ +
- +
-d) Write a function which takes a function $math[f] and two points $math[a, b] and approximates $math[\displaystyle \int_{a}^{b} f(x) dx] with **tolerance** ''​e=0.00001'',​ using the previous steps. +
- +
-<code haskell>​ +
- +
-integral :: (Double -> Double) -> Double -> Double -> Double +
- +
-</​code>​+