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: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[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 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> | + | |