####
4.6 KiB

title | description | author | date | published | tags | description | layout | comments |
---|---|---|---|---|---|---|---|---|

The Set Function | Modeling a mathematical set as a function. | Logan McGrath | 2022-06-16T21:29:05-0700 | 2022-06-16T22:22:26-0700 | functional programming, programming, scala, design patterns, combinators | What is a Set? A Set can tell you whether or not an value is a member of the Set. This means that a Set is merely a function, specifically of type A to Boolean. In this post I will explore the usage of **combinators** to build a Set from elementary functions alone. | post | true |

What is a `Set`

? A `Set`

can tell you whether or not an `value`

is a member of the `Set`

. This means that a `Set`

is merely a function, specifically of type `A => Boolean`

. In this post I will explore the usage of **combinators** to build a `Set`

from elementary functions alone.

## Modeling a `Set`

I'm going to use a trait to model a `Set`

:

:::{.numberLines}

```
trait Set[A] extends (A => Boolean)
```

:::

And define the most elementary of `Set`

s, the *empty Set* and

*singleton*:

`Set`

:::{.numberLines}

```
def empty[A](): Set[A] = _ => false
def singleton[A](value: A): Set[A] = test => test == value
val emptyInts = empty[Int]()
emptyInts(1) // false
emptyInts(2) // false
val justOne = singleton(1)
justOne(1) // true
justOne(2) // false
```

:::

But what about more complex sets?

## The `Set`

of natural numbers

The `Set`

of natural numbers requires more work to model. Specifically, it represents a lower bound of `0`

:

:::{.numberLines}

```
def nat(value: Int): Boolean = value >= 0
```

:::

But this is very concrete. A lower bound specifically is a type of *predicate*, and this can be parameterized:

:::{.numberLines}

```
def satisfy(predicate: A => Boolean): Set[A] = predicate
def nat: Set[Int] = satisfy(_ >= 0)
nat(-1) // false
nat(0) // true
nat(1) // true
```

:::

## Building a `Set`

How do I add values to the `Set`

? I have to be able to combine `Set`

s:

:::{.numberLines}

```
trait Set[A] extends (A => Boolean) {
def |[B >: A](other: Set[B]): Set[B] =
value => this(value) || other(value)
}
```

:::

By combining `Set`

s, I can now specify that a `value`

be one of two `value`

s in a `Set`

:

:::{.numberLines}

```
val oneOrTwo = singleton(1) | singleton(2)
oneOrTwo(1) // true
oneOrTwo(2) // true
oneOrTwo(3) // false
```

:::

But this `|`

operator doesn't allow me to set boundaries using predicates, as it represents a *disjoint Set*. This means

`value`

s must be present at least in one `Set`

or the other::::{.numberLines}

```
val singleDigits = satisfy(_ >= 0) | satisfy(_ <= 9)
singleDigits(10) // true
singleDigits(-1) // true
```

:::

I need another operator requiring membership in both `Set`

s, so that I can create a *joint Set*:

:::{.numberLines}

```
trait Set[A] extends (A => Boolean) {
def |[B >: A](other: Set[B]): Set[B] =
value => this(value) || other(value)
def &[B >: A](other: Set[B]): Set[B] =
value => this(value) && other(value)
}
```

:::

The `&`

operator now requires that the `value`

exists in *both* `Set`

s:

:::{.numberLines}

```
val singleDigits = satisfy(_ >= 0) & satisfy(_ <= 9)
singleDigits(2) // true
singleDigits(9) // true
singleDigits(10) // false
singleDigits(-1) // false
```

:::

### Building a complex `Set`

I'm going to build a weird `Set`

of numbers from `0-100`

, `102`

, `104`

, `220-230`

, and all numbers greater than `300`

that are divisible by `17`

:

:::{.numberLines}

```
val complexSet = (satisfy(_ >= 0) & satisfy(_ <= 100)) |
singleton(102) |
singleton(104) |
(satisfy(_ >= 220) | satisfy(_ <= 230)) |
(satisfy(_ >= 300) & satisfy(_ % 17 == 0))
complexSet(60) // true
complexSet(104) // true
complexSet(180) // false
complexSet(227) // true
complexSet(340) // true
complexSet(341) // false
```

:::

## Takeaway

Using functions alone, complex logic can be composed from atomic, testable building blocks. These functions that take other functions as arguments to produce new functions are referred to as **combinators** as they *combine* their capabilities.

Composing programs from small units ensures testability and ease in refactoring, as the single-responsibility principle is taken to its logical extreme with this technique.

### Why don't we use `Set`

s as functions?

This seems like a cool way to model `Set`

s, but that's about where the utility stops. As an exercise, it's fun to see what you can do to model `Set`

s using functions alone, as this is an excellent vehicle for learning functional composition with combinators. In practice this would produce a `Set`

with linear-time performance, and you don't want that.