Skip to content

Commit

Permalink
Concepts/functions generating functions (#589)
Browse files Browse the repository at this point in the history
* Added functions-generating-functions concept

* Added functions-generating-functions concept

* Made examples more readable by introducing new lines. Added .meta/config.json and updated links.json

* Deleted html file from functions-generating-functions concept, updated config.json, and made about.md have the same content as introduction.md
  • Loading branch information
JaumeAmoresDS committed Oct 20, 2023
1 parent d5bdff9 commit 3fe389c
Show file tree
Hide file tree
Showing 6 changed files with 322 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,6 @@ pom.xml.asc
.clj-kondo
.lsp
*.calva
**/*/*.ipynb_checkpoints
concepts/functions-generating-functions/introduction_files/
concepts/functions-generating-functions/*.html
4 changes: 4 additions & 0 deletions concepts/functions-generating-functions/.meta/config.json
Original file line number Diff line number Diff line change
@@ -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"]
}
142 changes: 142 additions & 0 deletions concepts/functions-generating-functions/about.md
Original file line number Diff line number Diff line change
@@ -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]
```
142 changes: 142 additions & 0 deletions concepts/functions-generating-functions/introduction.md
Original file line number Diff line number Diff line change
@@ -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]
```
26 changes: 26 additions & 0 deletions concepts/functions-generating-functions/links.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
5 changes: 5 additions & 0 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down

0 comments on commit 3fe389c

Please sign in to comment.