Skip to content

[Documentation] Trying to rewrite typeclasses-new #8147

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

Merged
merged 29 commits into from
Mar 30, 2020
Merged
Changes from 26 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
a5c8aef
trying to rewrite typeclasses-new
Jan 6, 2020
1bd517e
Wrap the last sentence in a whole "summary" section to avoid stray text
Jan 31, 2020
171a09a
Step2: rewrite the Functor part in more details
Jan 31, 2020
6b7e497
Step3: rewrite the Monad part (apart from the Reader monad)
Jan 31, 2020
74ff540
Fix typos
Jan 31, 2020
8118ed7
Rephrase assertTransformation simplification
Jan 31, 2020
a076eea
Fix typos on the reader monad
Jan 31, 2020
66ebcfb
@bishabosha's note
Jan 31, 2020
334c4b4
attempt at explaining the reader monad
Jan 31, 2020
cd501b6
less concrete and more accurate definition of a Functor
aesteve Jan 31, 2020
ff7ca15
definition of A functor, not THE functor ability
aesteve Jan 31, 2020
cf29e82
A functor for the type constructor F[_]
aesteve Jan 31, 2020
8607014
better phrasing for abstracting away Config
aesteve Jan 31, 2020
4c81c46
parameterised type with abstract members => trait
aesteve Jan 31, 2020
6b6cd39
oo polymorphism vs. parametric polymorphism
aesteve Jan 31, 2020
c6ec581
explaining the difference between OO polymorphism and ad-hoc polymorp…
aesteve Jan 31, 2020
716ae0c
better phrasing for conclusion
aesteve Jan 31, 2020
c15cd63
remove the "we don't care" part
aesteve Jan 31, 2020
8b85b34
using F as a substitution for every type that ca be mapped over
aesteve Jan 31, 2020
86615f6
Merge remote-tracking branch 'upstream/master' into doc/rework-typecl…
Feb 9, 2020
f2d7a6d
Merge remote-tracking branch 'origin/doc/rework-typeclasses-new' into…
Feb 9, 2020
df15a24
Trying to add an easy-to-grasp definition of type classes
Feb 9, 2020
4b04603
Proper 0.23 syntax
Feb 10, 2020
cb6c511
Proper 0.23 syntax
Feb 10, 2020
cd9834e
typo
Feb 10, 2020
bf7b005
no longer `given as`
Feb 10, 2020
9c1509b
typeclasses-new.md is now typeclasses.md
Feb 13, 2020
466bff9
Adapt to 0.23 latest
Feb 26, 2020
219bc3c
Use new type wildcard syntax
Mar 30, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
276 changes: 276 additions & 0 deletions docs/docs/reference/contextual/typeclasses-new.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
---
layout: doc-page
title: "Implementing Typeclasses"
---

A _typeclass_ is an abstract, parameterized type that lets you add new behavior to any closed data type without using sub-typing. This can be useful in multiple use-cases, for example:
* expressing how a type you don't own (from the standard or 3rd-party library) conforms to such behavior
* expressing such a behavior for multiple types without involving sub-typing relationships (one `extends` another) between those types (see: [ad hoc polymorphism](https://en.wikipedia.org/wiki/Ad_hoc_polymorphism) for instance)

Therefore in Scala 3, _typeclasses_ are just _traits_ with one or more parameters whose implementations are not defined through the `extends` keyword, but by **given instances**.
Here are some examples of usual typeclasses:

### Semigroups and monoids:

Here's the `Monoid` typeclass definition:

```scala
trait SemiGroup[T] {
@infix def (x: T) combine (y: T): T
}

trait Monoid[T] extends SemiGroup[T] {
def unit: T
}
```

An implementation of this `Monoid` typeclass for the type `String` can be the following:

```scala
given Monoid[String] {
def (x: String) combine (y: String): String = x.concat(y)
def unit: String = ""
}
```

Whereas for the type `Int` one could write the following:
```scala
given Monoid[Int] {
def (x: Int) combine (y: Int): Int = x + y
def unit: Int = 0
}
```

This monoid can now be used as _context bound_ in the following `combineAll` method:

```scala
def combineAll[T: Monoid](xs: List[T]): T =
xs.foldLeft(summon[Monoid[T]].unit)(_ combine _)
```

To get rid of the `summon[...]` we can define a `Monoid` object as follows:

```scala
object Monoid {
def apply[T](using m: Monoid[T]) = m
}
```

Which would allow to re-write the `combineAll` method this way:

```scala
def combineAll[T: Monoid](xs: List[T]): T =
xs.foldLeft(Monoid[T].unit)(_ combine _)
```

We can also benefit from [extension methods](extension-methods-new.html) to make this `combineAll` function accessible as a method on the `List` type:


```scala
def [T: Monoid](xs: List[T]).combineAll: T =
xs.foldLeft(Monoid[T].unit)(_ combine _)
```

Which allows one to write:

```scala
assert("ab" == List("a", "b").combineAll)
```
or:
```scala
assert(3 == List(1, 2).combineAll)
```

Copy link
Member

Choose a reason for hiding this comment

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

Thanks for doing this, it's a lot more with the style of the rest of the documentation. If you apply the same style to the Functors and Monads section this will be really nice.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

OK I'm not sure I have enough knowledge to re-write the Functor/Monad part in the same fashion (you need to understand properly to explain properly) but I will definitely give it a try.
If I'm struggling I'll ping you, so that you know you can merge this as is (best if the ennemy of good ;) ). But that's worth trying.

### Functors:

A `Functor` for a type provides the ability for its values to be "mapped over", i.e. apply a function that transforms inside a value while remembering its shape. For example, to modify every element of a collection without dropping or adding elements.
We can represent all types that can be "mapped over" with `F`. It's a type constructor: the type of its values becomes concrete when provided a type argument.
Therefore we write it `F[_]`, hinting that it is a type with internal details we can inspect.
The definition of a generic `Functor` would thus be written as:

```scala
trait Functor[F[_]] {
def map[A, B](original: F[A], mapper: A => B): F[B]
}
```

Which could read as follows: "A `Functor` for the type constructor `F[_]` represents the ability to transform `F[A]` to `F[B]` through the application of the `mapper` function whose type is `A => B`". We call the `Functor` definition here a _typeclass_.
This way, we could define an instance of `Functor` for the `List` type:
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
This way, we could define an instance of `Functor` for the `List` type:
This way, we could define an instance of `Functor` for the `List` type constructor:


```scala
given Functor[List] {
def map[A, B](original: List[A], mapper: A => B): List[B] =
original.map(mapper) // List already has a `map` method
}
```

With this `given` instance in scope, everywhere a `Functor` is expected, the compiler will accept a `List` to be used.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
With this `given` instance in scope, everywhere a `Functor` is expected, the compiler will accept a `List` to be used.
With this `given` instance in implicit scope, whenever a value of an abstract type with context bound of `Functor` is expected, a `List` value can be provided.


For instance, we may write such a testing method:
Copy link
Member

Choose a reason for hiding this comment

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

idk, too many instance words floating around

Suggested change
For instance, we may write such a testing method:
As a concrete use case, we may write a testing method with such a bound:

```scala
def assertTransformation[F[_]: Functor, A, B](expected: F[B], original: F[A], mapping: A => B): Unit =
assert(expected == summon[Functor[F]].map(original, mapping))
```

And use it this way, for example:

```scala
assertTransformation(List("a1", "b1"), List("a", "b"), elt => s"${elt}1")
```

That's a first step, but in practice we probably would like the `map` function to be a method directly accessible on the type `F`. So that we can call `map` directly on instances of `F`, and get rid of the `summon[Functor[F]]` part.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
That's a first step, but in practice we probably would like the `map` function to be a method directly accessible on the type `F`. So that we can call `map` directly on instances of `F`, and get rid of the `summon[Functor[F]]` part.
That's a first step, but in practice we probably would like the `map` function to be a method directly accessible on concrete instances of `F[_]`. So that we can call `map` directly, and get rid of the `summon[Functor[F]]` part.

As in the previous example of Monoids, [`extension` methods](extension-methods-new.html) help achieving that. Let's re-define the `Functor` _typeclass_ with extension methods.

```scala
trait Functor[F[_]] {
def [A, B](original: F[A]).map(mapper: A => B): F[B]
}
```

The instance of `Functor` for `List` now becomes:

```scala
given Functor[List] {
def [A, B](original: List[A]).map(mapper: A => B): List[B] =
original.map(mapper) // List already has a `map` method
}
```

It simplifies the `assertTransformation` method:

```scala
def assertTransformation[F[_]: Functor, A, B](expected: F[B], original: F[A], mapping: A => B): Unit =
assert(expected == original.map(mapping))
```

The `map` method is now directly used on `original` since it is of type `F[A]` (where `F` is a `Functor`).


### Monads

Now we have a `Functor` for `List`.

Applying the `List.map` ability with the following mapping function as parameter: `mapping: A => B` would result in a `List[B]`.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
Applying the `List.map` ability with the following mapping function as parameter: `mapping: A => B` would result in a `List[B]`.
Using our `Functor` to call `map` on a `List[A]` with an argument of type `A => B` would result in obtaining a `List[B]`.


Now, applying the `List.map` ability with the following mapping function as parameter: `mapping: A => List[B]` would result in a `List[List[B]]`.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
Now, applying the `List.map` ability with the following mapping function as parameter: `mapping: A => List[B]` would result in a `List[List[B]]`.
If instead, we map with a function `A => List[B]`, we would get a `List[List[B]]`.


To avoid avoid managing lists of lists, we may want to "flatten" the values in a single list.

That's where `Monad` enters the party. A `Monad` for type `F[_]` is a `Functor[F]` with 2 more abilities:
* the flatten ability we just described: turning `F[A]` to `F[B]` when given a `mapping: A => F[B]` function
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
* the flatten ability we just described: turning `F[A]` to `F[B]` when given a `mapping: A => F[B]` function
* the flatten capability we just described: turning `F[A]` to `F[B]` when given a `mapping: A => F[B]` function

* the ability to create `F[A]` from a single value `A`
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
* the ability to create `F[A]` from a single value `A`
* the ability to create `F[A]` from a single value `A`.
Together, these capabilities extend `Functor` to allow transformation of values that can change their shape. For example, dropping or adding elements to a collection.


Here is the translation of this definition in Scala 3:
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
Here is the translation of this definition in Scala 3:
Here is how we extend our `Functor` definition with the new capabilities:


```scala
trait Monad[F[_]] extends Functor[F] { // "A `Monad` for type `F[_]` is a `Functor[F]`" => thus has the `map` ability
def pure[A](x: A): F[A] // `pure` can construct F[A] from a single value A
def [A, B](x: F[A]).flatMap(f: A => F[B]): F[B] // the flattening ability is named `flatMap`, using extension methods as previous examples
def [A, B](x: F[A]).map(f: A => B) = x.flatMap(f `andThen` pure) // the `map(f)` ability is simply a combination of applying `f` then turning the result into an `F[A]` then applying `flatMap` to it
}
```

#### List

Let us declare the `Monad` ability for type `List`
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
Let us declare the `Monad` ability for type `List`
Let us declare a `Monad` for `List`:

```scala
given listMonad: Monad[List] {
def pure[A](x: A): List[A] =
List(x)
def [A, B](xs: List[A]).flatMap(f: A => List[B]): List[B] =
xs.flatMap(f) // let's rely on the existing `flatMap` method of `List`
}
```

`map` implementation is no longer needed.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
`map` implementation is no longer needed.
If we only defined a `Monad` for `List`, then a default implementation of `map` is already provided, as `map` on `List` is equivalent to a combination of `flatMap` and `pure`.


#### Option

`Option` is an other type having the same kind of behaviour:
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
`Option` is an other type having the same kind of behaviour:
`Option` is another type that has a definition for `Monad`:

* the `map` ability turning `Option[A]` into `Option[B]` if passed a function `f: A => B`
* the `flatMap` ability turning `Option[A]` into `Option[B]` if passed a function `f: A => Option[B]`
* the `pure` ability turning `A` into `Option[A]`

```scala
given optionMonad: Monad[Option] {
def pure[A](x: A): Option[A] =
Option(x)
def [A, B](xs: Option[A]).flatMap(f: A => Option[B]): Option[B] =
xs.flatMap(f) // let's rely on the existing `flatMap` method of `Option`
}
```

#### The Reader Monad

Another example of a `Monad` is the Reader Monad. It no longer acts on a type like `List` or `Option`, but on a function.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
Another example of a `Monad` is the Reader Monad. It no longer acts on a type like `List` or `Option`, but on a function.
Another example of a `Monad` is the reader monad. It doesn't act on a collection such as `List` or `Option`, but a function.

It can be used for example for combining functions that all need the same type of parameter. For instance multiple functions needing access to some configuration, context, environment variables, etc.

Let us have a `Config` type, and two functions using it:
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
Let us have a `Config` type, and two functions using it:
To explore the use case for reader, let us define a concrete `Config` trait, and two operations that require a `Config` to be provided:


```scala
trait Config
def compute(i: Int)(config: Config): String = ???
def show(str: String)(config: Config): Unit = ???
```

We may want to combine `compute` and `show` into a single function, accepting a `Config` as parameter, and showing the result of the computation.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
We may want to combine `compute` and `show` into a single function, accepting a `Config` as parameter, and showing the result of the computation.
We may want to combine `compute` and `show` into a single operation, accepting a `Config` as parameter, and showing the result of the computation.

If we had a `flatMap` function as in the examples above, we would be able to write the following:

```scala
def computeAndShow(i: Int): Config => Unit = compute(i).flatMap(show)
```

Let's define this `Monad` then. First, we are going to define a type named `ConfigDependent` representing a function that when passed a `Config` produces a `Result`.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
Let's define this `Monad` then. First, we are going to define a type named `ConfigDependent` representing a function that when passed a `Config` produces a `Result`.
Let's define a `Monad` to chain our operations with `flatMap`. First, we are going to define a type named `ConfigDependent` representing a function that when passed a `Config` produces a `Result`.


```scala
trait Config // the Config defined above
type ConfigDependent[Result] = Config => Result
```

The monad will look like this:

```scala
given configDependentMonad as Monad[ConfigDependent]
def [A, B](r: ConfigDependent[A]).flatMap(f: A => ConfigDependent[B]): ConfigDependent[B] =
config => f(r(config))(config)
def pure[A](x: A): ConfigDependent[A] =
config => x
```

The type `ConfigDependent` can be written using [type lambdas](../new-types/type-lambdas.html):

```scala
type ConfigDependent = [Result] =>> Config => Result
```

Using this syntax would turn the previous `configReaderMonad` into:


```scala
given configDependentMonad as Monad[[Result] =>> Config => Result]
def [A, B](r: Config => A).flatMap(f: A => Config => B): Config => B =
config => f(r(config))(config)
def pure[A](x: A): Config => A =
config => x
```



It is likely that we would like to use this pattern with other kinds of environments than our `Config` trait. The Reader monad allows us to abstract away `Config` as a type _parameter_, named `Ctx` in the following definition:

```scala
given readerMonad[Ctx] as Monad[[X] =>> Ctx => X] {
def [A, B](r: Ctx => A).flatMap(f: A => Ctx => B): Ctx => B =
ctx => f(r(ctx))(ctx)
def pure[A](x: A): Ctx => A =
ctx => x
}
```

### Summary

The definition of a _typeclass_ is expressed via a parameterised type with abstract members, such as a `trait`.
The main difference between object oriented polymorphism, and ad-hoc polymorphism with _typeclasses_, is how the definition of the _typeclass_ is implemented, in relation to the type it acts upon.
In the case of a _typeclass_, its implementation for a concrete type is expressed through a `given` term definition, which is supplied as an implicit argument alongside the value it acts upon. With object oriented polymorphism, the implementation is mixed into the parents of a class, and only a single term is required to perform a polymorphic operation.

To conclude, in addition to given instances, other constructs like extension methods, context bounds and type lambdas allow a concise and natural expression of _typeclasses_.