|
| 1 | +(**************************************************************************) |
| 2 | +(* *) |
| 3 | +(* Recursion *) |
| 4 | +(* *) |
| 5 | +(* I understand {b recursion} as a function that calls itself, or, that *) |
| 6 | +(* is defined in terms of itself. Its use cases are various: *) |
| 7 | +(* *) |
| 8 | +(* - {b Mathematics}: Factorial, fractals, fibonacci sequence. *) |
| 9 | +(* - {b Computer Science}: *) |
| 10 | +(* - Dynamic programming algorithms that solve problems by breaking *) |
| 11 | +(* them into smaller sub-problems (divide and conquer). *) |
| 12 | +(* - Tree-like data structures and their algorithms. *) |
| 13 | +(* - Graph theory algorithms. *) |
| 14 | +(* - Purely Functional programming, such as those algorithms that *) |
| 15 | +(* operate on iterative data structures (lists, sets, maps, etc). *) |
| 16 | +(* - {b Arts}: Matrioshka dolls, infinity mirror, droste effect. *) |
| 17 | +(* *) |
| 18 | +(* Functional programming languages rely heavily on recursion due to *) |
| 19 | +(* procedural loops being (mostly) rejected in favour of it. Take for *) |
| 20 | +(* example a purely FP language like Haskell; there is NO way to write *) |
| 21 | +(* loops so in order to iterate you are forced to think in terms of *) |
| 22 | +(* recursion. OCaml is not a purely FP language and provides both loops *) |
| 23 | +(* and mutable references for cases in which performance is needed. *) |
| 24 | +(* *) |
| 25 | +(* In OCaml, in order to mark a function as recursive, we need to add the *) |
| 26 | +(* [rec] keyword in front of [let], or pass an existing function to a *) |
| 27 | +(* {e fixing} function so that it works as intended. On top of that, the *) |
| 28 | +(* compiler guarantees that it will perform {b tail-call optimization} *) |
| 29 | +(* if it's possible. A tail-call optimized recursive function avoids *) |
| 30 | +(* creating multiple stack frames (leads to a {e stack overflow} if it's *) |
| 31 | +(* written in a way that the last call inside the function is the *) |
| 32 | +(* function itself with nothing in front or behind it, thus avoiding the *) |
| 33 | +(* need to backtrack. To achieve this, most programmers agree'd on a *) |
| 34 | +(* convention that involves an {e auxiliar} function defined inside or *) |
| 35 | +(* outside the main one, which in turn is called with some initial *) |
| 36 | +(* value for the accumulator and any dependency it might require. The *) |
| 37 | +(* main tradeoff is that some functions are incredibly complex to write *) |
| 38 | +(* with TCO in mind and may even require you to make use of stacks. *) |
| 39 | +(* *) |
| 40 | +(**************************************************************************) |
| 41 | + |
| 42 | +module type Recursion = sig |
| 43 | + (** [print_range a b] prints lines with integer numbers from [a] to [b]. If no |
| 44 | + value is passed to [a], it will default to [0]. The order in which the |
| 45 | + printing happens depends on if [a > b] (descending order) or if [a <= b] |
| 46 | + (ascending order). This function is {b tail-call optimized}. *) |
| 47 | + val print_range : ?a:int -> int -> unit |
| 48 | + |
| 49 | + (** [factorial n] is the result of multiplying [n * n-1 * n-2 * ...] until |
| 50 | + [n] reaches 0 (base case that results in 1). The recursive mathematical |
| 51 | + definition of the {e factorial} is: {m n! = n \cdot (n - 1)!}. This |
| 52 | + function is {b not tail-call optimized}. *) |
| 53 | + val factorial : float -> float |
| 54 | + |
| 55 | + (** [factorial_tc n] is the {b tail-call optimized} version of {! factorial}. |
| 56 | +
|
| 57 | + @raise Failure if [n < 0] (negative factorials are impossible). *) |
| 58 | + val factorial_tco : float -> float |
| 59 | + |
| 60 | + (** [fib n] is the nth integer in the fibonacci sequence |
| 61 | + ([0, 1, 1, 2, 3, 5, 8, 13, ...]). [n] represents the index in the |
| 62 | + aforementioned sequence, which is computed by adding the previous two |
| 63 | + numbers starting with [0] and [1]. It can be solved iteratively with |
| 64 | + a [for] loop but it's best represented as a recursive evaluation tree: |
| 65 | +
|
| 66 | + {[ |
| 67 | + ⨍(5) |
| 68 | + / \ |
| 69 | + ⨍(4) + ⨍(3) = 5 |
| 70 | + / \ / \ |
| 71 | + ⨍(3) + ⨍(2) = 3 ⨍(2) + ⨍(1) = 2 |
| 72 | + / \ / \ |
| 73 | + ⨍(2) + ⨍(1) = 2 ⨍(1) + ⨍(0) = 1 |
| 74 | + / \ |
| 75 | + ⨍(1) + ⨍(0) = 1 |
| 76 | + ]} |
| 77 | +
|
| 78 | + Few things can be observed in this evaluation tree: |
| 79 | +
|
| 80 | + + From a very small number ([5]), a lot of branches and individual calls |
| 81 | + to [fib] are created, making it exponentially inefficient. This is |
| 82 | + called a {b naive implementation} of a dynamic programming algorithm. |
| 83 | + + A lot of calls to [fib n] share the same input [n], making it possible |
| 84 | + to optimize using a technique called {b memoization}. |
| 85 | +
|
| 86 | + It is possible to run the 2 branches [n-1] and [n-2] in parallel which |
| 87 | + reduces the execution time by a decent margin (depending on how many |
| 88 | + {e physical cores} the CPU has), though it does come with a cost. |
| 89 | +
|
| 90 | + See https://v2.ocaml.org/releases/5.0/manual/parallelism.html. *) |
| 91 | + val fib : int -> int |
| 92 | + |
| 93 | + (** [fib_memo n] is the {b memoized} version of [fib n]. Memoization is a |
| 94 | + of optimizing dynamic programic algorithms and expensive functions by |
| 95 | + having a cache map (implemented as a key-value pair) that saves computed |
| 96 | + results to it and if the function receives the same input(s) again, the |
| 97 | + cached value can be retrieved thus saving tons of execution time. |
| 98 | +
|
| 99 | + [fib_memo]'s cache is implemented with a [Hashtbl] (initial size [100]). |
| 100 | +
|
| 101 | + @raise Failure if [n < 0]. [n] should be positive (or [0]). *) |
| 102 | + val fib_memo : int -> int |
| 103 | + |
| 104 | + (** [fib_tco n] is the {b tail-call optimized} version of [fib n]. |
| 105 | +
|
| 106 | + @raise Failure if [n < 0]. [n] should be positive (or [0]). *) |
| 107 | + val fib_tco : int -> int |
| 108 | +end |
| 109 | + |
| 110 | +module Exercise6 : Recursion = struct |
| 111 | + let rec print_range ?(a = 0) b = |
| 112 | + let delta = if a > b then -1 else 1 in |
| 113 | + print_endline (string_of_int a); |
| 114 | + if a <> b then print_range ~a:(a + delta) b |
| 115 | + ;; |
| 116 | + |
| 117 | + (* DIFICULTAD EXTRA (opcional): |
| 118 | + ---------------------------- |
| 119 | + Utiliza el concepto de recursividad para: |
| 120 | + - Calcular el factorial de un número concreto (la función recibe ese número). |
| 121 | + - Calcular el valor de un elemento concreto (según su posición) en la |
| 122 | + sucesión de Fibonacci (la función recibe la posición). |
| 123 | + *) |
| 124 | + |
| 125 | + let rec factorial n = if n < 2. then 1. else n *. factorial (n -. 1.) |
| 126 | + |
| 127 | + let factorial_tco n = |
| 128 | + let rec aux ~acc n = |
| 129 | + if n < 0. |
| 130 | + then failwith "Negative factorial" |
| 131 | + else if n < 2. |
| 132 | + then acc |
| 133 | + else aux ~acc:(acc *. n) (n -. 1.) |
| 134 | + in |
| 135 | + aux ~acc:1. n |
| 136 | + ;; |
| 137 | + |
| 138 | + let rec fib n = if n < 2 then n else fib (n - 1) + fib (n - 2) |
| 139 | + |
| 140 | + let fib_memo = |
| 141 | + let cache = Hashtbl.create 100 in |
| 142 | + let rec aux n = |
| 143 | + if n < 0 |
| 144 | + then failwith "Negative fibonacci sequence index" |
| 145 | + else if n < 2 |
| 146 | + then n |
| 147 | + else begin |
| 148 | + match Hashtbl.find_opt cache n with |
| 149 | + | Some n -> n |
| 150 | + | None -> |
| 151 | + let result = aux (n - 1) + aux (n - 2) in |
| 152 | + Hashtbl.replace cache n result; |
| 153 | + result |
| 154 | + end |
| 155 | + in |
| 156 | + aux |
| 157 | + ;; |
| 158 | + |
| 159 | + let fib_tco n = |
| 160 | + let rec aux ~b ~a i = if i <= 0 then a else aux ~a:b ~b:(a + b) (i - 1) in |
| 161 | + if n < 0 |
| 162 | + then failwith "Negative fibonacci sequence index" |
| 163 | + else aux ~a:0 ~b:1 n |
| 164 | + ;; |
| 165 | +end |
| 166 | +;; |
| 167 | + |
| 168 | +begin |
| 169 | + let open Exercise6 in |
| 170 | + let open Printf in |
| 171 | + print_range ~a:100 0; |
| 172 | + printf "Factorial of 5: %f\n" (factorial 5.); |
| 173 | + printf |
| 174 | + "Tail-Call Optimized Factorial of -5: %s\n" |
| 175 | + (try factorial_tco (-5.) |> string_of_float with |
| 176 | + | Failure _ -> "ERROR"); |
| 177 | + printf "Tail-Call Optimized Factorial of 50.0: %f\n" (factorial_tco 50.); |
| 178 | + (* Running [fib 37] is expensive so you will notice a slight delay*) |
| 179 | + printf "37th number in the fibonacci sequence (naive): %d\n" (fib 37); |
| 180 | + printf |
| 181 | + "100th number in the fibonacci sequence (memoized): %d\n" |
| 182 | + (fib_memo 100); |
| 183 | + printf "52nd number in the fibonacci sequence (tco): %d\n" (fib_tco 52) |
| 184 | +end |
| 185 | + |
| 186 | +(* Output of [ocaml luishendrix92.ml]: |
| 187 | +
|
| 188 | + 100 |
| 189 | + 99 |
| 190 | + 98 |
| 191 | + 97 |
| 192 | + 96 |
| 193 | + 95 |
| 194 | + 94 |
| 195 | + 93 |
| 196 | + 92 |
| 197 | + 91 |
| 198 | + 90 |
| 199 | + 89 |
| 200 | + 88 |
| 201 | + 87 |
| 202 | + 86 |
| 203 | + 85 |
| 204 | + 84 |
| 205 | + 83 |
| 206 | + 82 |
| 207 | + 81 |
| 208 | + 80 |
| 209 | + 79 |
| 210 | + 78 |
| 211 | + 77 |
| 212 | + 76 |
| 213 | + 75 |
| 214 | + 74 |
| 215 | + 73 |
| 216 | + 72 |
| 217 | + 71 |
| 218 | + 70 |
| 219 | + 69 |
| 220 | + 68 |
| 221 | + 67 |
| 222 | + 66 |
| 223 | + 65 |
| 224 | + 64 |
| 225 | + 63 |
| 226 | + 62 |
| 227 | + 61 |
| 228 | + 60 |
| 229 | + 59 |
| 230 | + 58 |
| 231 | + 57 |
| 232 | + 56 |
| 233 | + 55 |
| 234 | + 54 |
| 235 | + 53 |
| 236 | + 52 |
| 237 | + 51 |
| 238 | + 50 |
| 239 | + 49 |
| 240 | + 48 |
| 241 | + 47 |
| 242 | + 46 |
| 243 | + 45 |
| 244 | + 44 |
| 245 | + 43 |
| 246 | + 42 |
| 247 | + 41 |
| 248 | + 40 |
| 249 | + 39 |
| 250 | + 38 |
| 251 | + 37 |
| 252 | + 36 |
| 253 | + 35 |
| 254 | + 34 |
| 255 | + 33 |
| 256 | + 32 |
| 257 | + 31 |
| 258 | + 30 |
| 259 | + 29 |
| 260 | + 28 |
| 261 | + 27 |
| 262 | + 26 |
| 263 | + 25 |
| 264 | + 24 |
| 265 | + 23 |
| 266 | + 22 |
| 267 | + 21 |
| 268 | + 20 |
| 269 | + 19 |
| 270 | + 18 |
| 271 | + 17 |
| 272 | + 16 |
| 273 | + 15 |
| 274 | + 14 |
| 275 | + 13 |
| 276 | + 12 |
| 277 | + 11 |
| 278 | + 10 |
| 279 | + 9 |
| 280 | + 8 |
| 281 | + 7 |
| 282 | + 6 |
| 283 | + 5 |
| 284 | + 4 |
| 285 | + 3 |
| 286 | + 2 |
| 287 | + 1 |
| 288 | + 0 |
| 289 | + Factorial of 5: 120.000000 |
| 290 | + Tail-Call Optimized Factorial of -5: ERROR |
| 291 | + Tail-Call Optimized Factorial of 50.0: 30414093201713375576366966406747986832057064836514787179557289984.000000 |
| 292 | + 37th number in the fibonacci sequence (naive): 24157817 |
| 293 | + 100th number in the fibonacci sequence (memoized): 3736710778780434371 |
| 294 | + 52nd number in the fibonacci sequence (tco): 32951280099 |
| 295 | +*) |
0 commit comments