- Notifications
You must be signed in to change notification settings - Fork 1.1k
[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
Changes from 7 commits
a5c8aef 1bd517e 171a09a 6b7e497 74ff540 8118ed7 a076eea 66ebcfb 334c4b4 cd501b6 ff7ca15 cf29e82 8607014 4c81c46 6b6cd39 c6ec581 716ae0c c15cd63 8b85b34 86615f6 f2d7a6d df15a24 4b04603 cb6c511 cd9834e bf7b005 9c1509b 466bff9 219bc3c File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| | @@ -3,12 +3,13 @@ layout: doc-page | |||||||||
| title: "Implementing Typeclasses" | ||||||||||
| --- | ||||||||||
| | ||||||||||
| Given instances, extension methods and context bounds | ||||||||||
| allow a concise and natural expression of _typeclasses_. Typeclasses are just traits | ||||||||||
| with canonical implementations defined by given instances. Here are some examples of standard typeclasses: | ||||||||||
| In Scala 3, _typeclasses_ are just traits whose implementation are defined by given instances. | ||||||||||
| Here are some examples of standard typeclasses: | ||||||||||
| | ||||||||||
| ### Semigroups and monoids: | ||||||||||
| | ||||||||||
| Here's the `Monoid` typeclass definition: | ||||||||||
| | ||||||||||
| ```scala | ||||||||||
| trait SemiGroup[T] { | ||||||||||
| @infix def (x: T) combine (y: T): T | ||||||||||
| | @@ -17,50 +18,204 @@ trait SemiGroup[T] { | |||||||||
| trait Monoid[T] extends SemiGroup[T] { | ||||||||||
| def unit: T | ||||||||||
| } | ||||||||||
| ``` | ||||||||||
| | ||||||||||
| object Monoid { | ||||||||||
| def apply[T] with (m: Monoid[T]) = m | ||||||||||
| } | ||||||||||
| An implementation of this `Monoid` typeclass for the type `String` can be the following: | ||||||||||
| | ||||||||||
| ```scala | ||||||||||
| given as 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 as Monoid[Int] { | ||||||||||
| def (x: Int) combine (y: Int): Int = x + y | ||||||||||
| def unit: Int = 0 | ||||||||||
| } | ||||||||||
| ``` | ||||||||||
| | ||||||||||
| def sum[T: Monoid](xs: List[T]): T = | ||||||||||
| 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] with (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 _) | ||||||||||
| ``` | ||||||||||
| | ||||||||||
| ### Functors and monads: | ||||||||||
| 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) | ||||||||||
| ``` | ||||||||||
| | ||||||||||
| ### Functors: | ||||||||||
| | ||||||||||
| A `Functor` represents the ability for a type containing zero or more elements to be "mapped over", i.e. apply a function to every of its elements. | ||||||||||
aesteve marked this conversation as resolved. Outdated Show resolved Hide resolved | ||||||||||
| Let's name our "type containing zero or more elements" `F`. It's a [higher-kinded type](http://guillaume.martres.me/publications/dotty-hk.pdf), since it contains elements of an other type. | ||||||||||
aesteve marked this conversation as resolved. Outdated Show resolved Hide resolved | ||||||||||
| Therefore we'll write it `F[_]` since we don't really care about the type of the elements it contains. | ||||||||||
aesteve marked this conversation as resolved. Outdated Show resolved Hide resolved | ||||||||||
| The definition of the `Functor` ability would thus be written as: | ||||||||||
aesteve marked this conversation as resolved. Outdated Show resolved Hide resolved | ||||||||||
| | ||||||||||
| ```scala | ||||||||||
| trait Functor[F[_]] { | ||||||||||
| def map[A, B](original: F[A], mapper: A => B): F[B] | ||||||||||
| } | ||||||||||
| ``` | ||||||||||
| | ||||||||||
| Which could read as follows: "The `Functor` ability for a wrapper type `F` represents the ability to transform `F[A]` to `F[B]` through the application of the `mapper` function whose type is `A => B`". | ||||||||||
aesteve marked this conversation as resolved. Outdated Show resolved Hide resolved | ||||||||||
| 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: | |
| This way, we could define an instance of `Functor` for the `List` type constructor: |
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| 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. |
Outdated
There was a problem hiding this comment.
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
| For instance, we may write such a testing method: | |
| As a concrete use case, we may write a testing method with such a bound: |
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| 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. |
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| 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]`. |
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| 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]]`. |
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| That's where `Monad` enter the party. A `Monad` for type `F[_]` is a `Functor[F]` with 2 more abilities: | |
| That's where `Monad` enters the party. A `Monad` for type `F[_]` is a `Functor[F]` with 2 more capabilities: |
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| * 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 |
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| * 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. |
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| Here is the translation of this definition in Scala 3: | |
| Here is how we extend our `Functor` definition with the new capabilities: |
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| Let us declare the `Monad` ability for type `List` | |
| Let us declare a `Monad` for `List`: |
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| `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`. |
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| `Option` is an other type having the same kind of behaviour: | |
| `Option` is another type that has a definition for `Monad`: |
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| 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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.