Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Concepts/functions generating functions #589

Merged
merged 4 commits into from Oct 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
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
@@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The about page should contain the same content as introduction.md (or more, but not less)

@@ -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
@@ -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
@@ -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
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