diff --git a/.gitignore b/.gitignore index 51a74af27..a943c431f 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,6 @@ pom.xml.asc .clj-kondo .lsp *.calva +**/*/*.ipynb_checkpoints +concepts/functions-generating-functions/introduction_files/ +concepts/functions-generating-functions/*.html diff --git a/concepts/functions-generating-functions/.meta/config.json b/concepts/functions-generating-functions/.meta/config.json new file mode 100644 index 000000000..91f270df9 --- /dev/null +++ b/concepts/functions-generating-functions/.meta/config.json @@ -0,0 +1,4 @@ +{ + "blurb": "We explore here four important cases of high-order functions that are used to generate new functions: `partial`, `comp`, `memoize` and `juxt`", + "authors": ["JaumeAmoresDS"] + } \ No newline at end of file diff --git a/concepts/functions-generating-functions/about.md b/concepts/functions-generating-functions/about.md new file mode 100644 index 000000000..59fd05a88 --- /dev/null +++ b/concepts/functions-generating-functions/about.md @@ -0,0 +1,142 @@ +# About + +Being a functional language, functions in Clojure are first-class citizens. This means that they can be passed to and generated by other functions just like data. Clojure in particular comes with a rich set of high-order functions that derive new functions based on existing ones. We will explore here four important cases: `partial`, `comp`, `memoize` and `juxt`. These function-generating functions fall into a broader category of higher-order functions, such as `map`, `reduce`, `apply`, `complement`, to name a few, which operate on existing functions. + +## partial + +In general, given an existing function `my-function`, with parameters `p1, p2, ..., pN` we can fix some of its parameters `p1, p2, ..., pM` to have constant values `v1, v2, ..., vM`. We do so by applying `partial` as follows: `(partial my-function v1 v2 ... vM)`. + +The result is a new function which applies the original one `my-function` by fixing `p1=v1, p2=v2, ..., pM=vM`. Our new function takes as input the remaining parameters `pM+1, pM+2, ..., pN`, whose values have not been indicated when applying `partial`: + +```clojure +(def my-new-function + (partial my-function v1 v2 ... vM)) +(my-new-function xM+1 xM+2 ... xN) +; => equivalent to (my-function v1 v2 ... vM xM+1 xM+2 ... xN) +``` + +As a simple example, let's define a function `inc-by-9` which increases by 9, a fixed amount, whatever we pass-in. We could implement such function as follows: + +```clojure +(def inc-by-9 (partial + 9)) +(inc-by-9 5) +; => 14 +``` + +As a second example, we have a function `generic-greetings` that uses the name of the person we wish to greet along with initial and final text messages: + +```clojure +(defn generic-greetings + [initial-text final-text name] + (println (str initial-text name final-text))) +``` + +And use `partial` to always use a specific greetings message: + +```clojure +(def say-hello-and-how-are-you-doing + (partial generic-greetings "Hello " ", how are you doing?")) +(say-hello-and-how-are-you-doing "Mary") +; => Hello Mary, how are you doing? +``` + +## comp + +`comp` can be used to create a composition of any number of functions we want to compose. In general, composing `N` functions `f1, f2, ..., fN` means applying those functions in sequential order, passing the ouput of the previous function to the next one. In mathematical notation this is expressed as: + +``` +f1 (f2 (... fN(x))) +``` + +In clojure, this is equivalent to doing: +```clojure +(f1 (f2 (... (fN x)))) +``` + +By using comp, we can create a new function which performs this composition for us: + +```clojure +(def my-function-composition + (comp f1 f2 ... fN)) +(my-function-composition x) +; equivalent to +(f1 (f2 (... (fN x)))) +``` + +As an example, let's say we want to sum a series of numbers and then multiply the result by 6. We can do so as follows: + + +```clojure +(def six-times-result-sum + (comp (partial * 6) +)) +(six-times-result-sum 3 2) +; = ((partial * 6) (+ 3 2)) +; = (* 6 (+ 3 2)) +; = 30 +``` + +## memoize + +`memoize` allows to reuse previously computed results associated with a given input. Given the same input, the *memoized* function returns the same result without having to recompute it again. It takes advantage of the fact that `clojure` functions are, by default, *referentially transparent*: given the same input, they always produce the same output, which makes it possible to reuse previous computations with ease. + +In order to see how this works, let us use a synthetic case where the function sleeps for two seconds before producing the output, so we can easily compare the *computation time* before and after using `memoize`. + +```clojure +(defn my-time-consuming-fn + "Original, time-consuming function" + [x] + (Thread/sleep 2000) + (* x 2) +) + +; We measure the time it takes the original function +(time (my-time-consuming-fn 3)) +; => "Elapsed time: 2007.785622 msecs" +; => 6 + +; We define a memoized function +(def my-memoized-fn + (memoize my-time-consuming-fn) ) + +; We call the memoized function and measure its execution time. +; The first execution actually runs the function, taking +; similar time as the original. +(time (my-memoized-fn 3)) +; => "Elapsed time: 2001.364052 msecs" +; => 6 + +; Subsequent calls reuse the previous computation, taking less +; time. Time is further reduced in additional calls. +(time (my-memoized-fn 3)) +; => "Elapsed time: 1.190291 msecs" +; => 6 + +(time (my-memoized-fn 3)) +; => "Elapsed time: 0.043701 msecs" +; => 6 + +; Changing the input makes the function be executed, so that +; we observe a similar computation time as the original +(time (my-memoized-fn 4)) +; => "Elapsed time: 2000.29306 msecs" +; => 8 + +; Again, repeating the same input will make the +; execution be skipped, and the associated output returned +(time (my-memoized-fn 4)) +; => "Elapsed time: 0.043701 msecs" +; => 8 +``` + +## juxt + +`juxt` generates a function that applies, in left to right order, the functions passed to it. The result is a vector with the individual results of each function as components of the vector: + +```clojure +((juxt f g h) x) ; => [(f x) (g x) (h x)] +``` + +As a simple example, we generate a function that computes the product of x by successive factors, from 2 to 5: +```clojure +((juxt (partial * 2) (partial * 3) (partial * 4) (partial * 5)) 10) ; => [20 30 40 50] +``` \ No newline at end of file diff --git a/concepts/functions-generating-functions/introduction.md b/concepts/functions-generating-functions/introduction.md new file mode 100644 index 000000000..2064be992 --- /dev/null +++ b/concepts/functions-generating-functions/introduction.md @@ -0,0 +1,142 @@ +# Introduction + +Being a functional language, functions in Clojure are first-class citizens. This means that they can be passed to and generated by other functions just like data. Clojure in particular comes with a rich set of high-order functions that derive new functions based on existing ones. We will explore here four important cases: `partial`, `comp`, `memoize` and `juxt`. These function-generating functions fall into a broader category of higher-order functions, such as `map`, `reduce`, `apply`, `complement`, to name a few, which operate on existing functions. + +## partial + +In general, given an existing function `my-function`, with parameters `p1, p2, ..., pN` we can fix some of its parameters `p1, p2, ..., pM` to have constant values `v1, v2, ..., vM`. We do so by applying `partial` as follows: `(partial my-function v1 v2 ... vM)`. + +The result is a new function which applies the original one `my-function` by fixing `p1=v1, p2=v2, ..., pM=vM`. Our new function takes as input the remaining parameters `pM+1, pM+2, ..., pN`, whose values have not been indicated when applying `partial`: + +```clojure +(def my-new-function + (partial my-function v1 v2 ... vM)) +(my-new-function xM+1 xM+2 ... xN) +; => equivalent to (my-function v1 v2 ... vM xM+1 xM+2 ... xN) +``` + +As a simple example, let's define a function `inc-by-9` which increases by 9, a fixed amount, whatever we pass-in. We could implement such function as follows: + +```clojure +(def inc-by-9 (partial + 9)) +(inc-by-9 5) +; => 14 +``` + +As a second example, we have a function `generic-greetings` that uses the name of the person we wish to greet along with initial and final text messages: + +```clojure +(defn generic-greetings + [initial-text final-text name] + (println (str initial-text name final-text))) +``` + +And use `partial` to always use a specific greetings message: + +```clojure +(def say-hello-and-how-are-you-doing + (partial generic-greetings "Hello " ", how are you doing?")) +(say-hello-and-how-are-you-doing "Mary") +; => Hello Mary, how are you doing? +``` + +## comp + +`comp` can be used to create a composition of any number of functions we want to compose. In general, composing `N` functions `f1, f2, ..., fN` means applying those functions in sequential order, passing the ouput of the previous function to the next one. In mathematical notation this is expressed as: + +``` +f1 (f2 (... fN(x))) +``` + +In clojure, this is equivalent to doing: +```clojure +(f1 (f2 (... (fN x)))) +``` + +By using comp, we can create a new function which performs this composition for us: + +```clojure +(def my-function-composition + (comp f1 f2 ... fN)) +(my-function-composition x) +; equivalent to +(f1 (f2 (... (fN x)))) +``` + +As an example, let's say we want to sum a series of numbers and then multiply the result by 6. We can do so as follows: + + +```clojure +(def six-times-result-sum + (comp (partial * 6) +)) +(six-times-result-sum 3 2) +; = ((partial * 6) (+ 3 2)) +; = (* 6 (+ 3 2)) +; = 30 +``` + +## memoize + +`memoize` allows to reuse previously computed results associated with a given input. Given the same input, the *memoized* function returns the same result without having to recompute it again. It takes advantage of the fact that `clojure` functions are, by default, *referentially transparent*: given the same input, they always produce the same output, which makes it possible to reuse previous computations with ease. + +In order to see how this works, let us use a synthetic case where the function sleeps for two seconds before producing the output, so we can easily compare the *computation time* before and after using `memoize`. + +```clojure +(defn my-time-consuming-fn + "Original, time-consuming function" + [x] + (Thread/sleep 2000) + (* x 2) +) + +; We measure the time it takes the original function +(time (my-time-consuming-fn 3)) +; => "Elapsed time: 2007.785622 msecs" +; => 6 + +; We define a memoized function +(def my-memoized-fn + (memoize my-time-consuming-fn) ) + +; We call the memoized function and measure its execution time. +; The first execution actually runs the function, taking +; similar time as the original. +(time (my-memoized-fn 3)) +; => "Elapsed time: 2001.364052 msecs" +; => 6 + +; Subsequent calls reuse the previous computation, taking less +; time. Time is further reduced in additional calls. +(time (my-memoized-fn 3)) +; => "Elapsed time: 1.190291 msecs" +; => 6 + +(time (my-memoized-fn 3)) +; => "Elapsed time: 0.043701 msecs" +; => 6 + +; Changing the input makes the function be executed, so that +; we observe a similar computation time as the original +(time (my-memoized-fn 4)) +; => "Elapsed time: 2000.29306 msecs" +; => 8 + +; Again, repeating the same input will make the +; execution be skipped, and the associated output returned +(time (my-memoized-fn 4)) +; => "Elapsed time: 0.043701 msecs" +; => 8 +``` + +## juxt + +`juxt` generates a function that applies, in left to right order, the functions passed to it. The result is a vector with the individual results of each function as components of the vector: + +```clojure +((juxt f g h) x) ; => [(f x) (g x) (h x)] +``` + +As a simple example, we generate a function that computes the product of x by successive factors, from 2 to 5: +```clojure +((juxt (partial * 2) (partial * 3) (partial * 4) (partial * 5)) 10) ; => [20 30 40 50] +``` \ No newline at end of file diff --git a/concepts/functions-generating-functions/links.json b/concepts/functions-generating-functions/links.json new file mode 100644 index 000000000..be49344d7 --- /dev/null +++ b/concepts/functions-generating-functions/links.json @@ -0,0 +1,26 @@ +[ + { + "url": "https://clojure.github.io/clojure/clojure.core-api.html#clojure.core/partial", + "description": "Clojure API: partial" + }, + { + "url": "https://clojure.github.io/clojure/clojure.core-api.html#clojure.core/comp", + "description": "Clojure API: comp" + }, + { + "url": "https://clojure.github.io/clojure/clojure.core-api.html#clojure.core/comp", + "description": "Clojure API: comp" + }, + { + "url": "https://clojuredocs.org/clojure.core/memoize", + "description": "Clojure docs: memoize" + }, + { + "url": "https://clojuredocs.org/clojure.core/juxt", + "description": "Clojure docs: juxt" + }, + { + "url": "https://clojure.org/guides/higher_order_functions", + "description": "Clojure guides: Higher Order Functions" + } +] \ No newline at end of file diff --git a/config.json b/config.json index c3590b173..698bace5c 100644 --- a/config.json +++ b/config.json @@ -1087,6 +1087,11 @@ "uuid": "75d16185-4ca9-49d1-969f-3dd6fc377b96", "slug": "closures", "name": "Closures" + }, + { + "uuid": "9d9d7cf1-8504-4f7b-b2a0-1b3b638dc0d9", + "slug": "functions-generating-functions", + "name": "Functions generating functions" } ], "key_features": [