Differences
This shows you the differences between two versions of the page.
| Both sides previous revision Previous revision Next revision | Previous revision | ||
|
pp:2023:haskell:l08 [2023/04/25 19:14] 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> | ||
| Line 68: | Line 183: | ||
| {- | {- | ||
| with 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[f] and two points $math[a, b] and 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 an 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> | + | |