You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Given instances, extension methods and context bounds
7
-
allow a concise and natural expression of _typeclasses_. Typeclasses are just traits
8
-
with canonical implementations defined by given instances. Here are some examples of standard typeclasses:
6
+
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:
7
+
* expressing how a type you don't own (from the standard or 3rd-party library) conforms to such behavior
8
+
* 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)
9
+
10
+
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**.
11
+
Here are some examples of usual typeclasses:
9
12
10
13
### Semigroups and monoids:
11
14
15
+
Here's the `Monoid` typeclass definition:
16
+
12
17
```scala
13
18
traitSemiGroup[T] {
14
19
def (x: T) combine (y: T):T
@@ -17,50 +22,257 @@ trait SemiGroup[T] {
17
22
traitMonoid[T] extendsSemiGroup[T] {
18
23
defunit:T
19
24
}
25
+
```
20
26
21
-
objectMonoid {
22
-
defapply[T](usingm: Monoid[T]) = m
23
-
}
27
+
An implementation of this `Monoid` typeclass for the type `String` can be the following:
Whereas for the type `Int` one could write the following:
37
+
```scala
30
38
givenMonoid[Int] {
31
39
def (x: Int) combine (y: Int):Int= x + y
32
40
defunit:Int=0
33
41
}
42
+
```
43
+
44
+
This monoid can now be used as _context bound_ in the following `combineAll` method:
45
+
46
+
```scala
47
+
defcombineAll[T:Monoid](xs: List[T]):T=
48
+
xs.foldLeft(summon[Monoid[T]].unit)(_ combine _)
49
+
```
50
+
51
+
To get rid of the `summon[...]` we can define a `Monoid` object as follows:
52
+
53
+
```scala
54
+
objectMonoid {
55
+
defapply[T](usingm: Monoid[T]) = m
56
+
}
57
+
```
34
58
35
-
defsum[T:Monoid](xs: List[T]):T=
59
+
Which would allow to re-write the `combineAll` method this way:
60
+
61
+
```scala
62
+
defcombineAll[T:Monoid](xs: List[T]):T=
36
63
xs.foldLeft(Monoid[T].unit)(_ combine _)
37
64
```
38
65
39
-
### Functors and monads:
66
+
We can also benefit from [extension methods](extension-methods-new.html) to make this `combineAll` function accessible as a method on the `List` type:
67
+
68
+
69
+
```scala
70
+
def [T:Monoid](xs: List[T]).combineAll:T=
71
+
xs.foldLeft(Monoid[T].unit)(_ combine _)
72
+
```
73
+
74
+
Which allows one to write:
75
+
76
+
```scala
77
+
assert("ab"==List("a", "b").combineAll)
78
+
```
79
+
or:
80
+
```scala
81
+
assert(3==List(1, 2).combineAll)
82
+
```
83
+
84
+
### Functors:
85
+
86
+
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.
87
+
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.
88
+
Therefore we write it `F[?]`, hinting that it is a type with internal details we can inspect.
89
+
The definition of a generic `Functor` would thus be written as:
90
+
91
+
```scala
92
+
traitFunctor[F[?]] {
93
+
defmap[A, B](original: F[A], mapper: A=>B):F[B]
94
+
}
95
+
```
96
+
97
+
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_.
98
+
This way, we could define an instance of `Functor` for the `List` type:
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.
122
+
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.
defassertTransformation[F[?]:Functor, A, B](expected: F[B], original: F[A], mapping: A=>B):Unit=
143
+
assert(expected == original.map(mapping))
144
+
```
145
+
146
+
The `map` method is now directly used on `original` since it is of type `F[A]` (where `F` is a `Functor`).
147
+
148
+
149
+
### Monads
150
+
151
+
Now we have a `Functor` for `List`.
152
+
153
+
Applying the `List.map` ability with the following mapping function as parameter: `mapping: A => B` would result in a `List[B]`.
154
+
155
+
Now, applying the `List.map` ability with the following mapping function as parameter: `mapping: A => List[B]` would result in a `List[List[B]]`.
49
156
50
-
defpure[A](x: A):F[A]
157
+
To avoid avoid managing lists of lists, we may want to "flatten" the values in a single list.
158
+
159
+
That's where `Monad` enters the party. A `Monad` for type `F[?]` is a `Functor[F]` with 2 more abilities:
160
+
* the flatten ability we just described: turning `F[A]` to `F[B]` when given a `mapping: A => F[B]` function
161
+
* the ability to create `F[A]` from a single value `A`
162
+
163
+
Here is the translation of this definition in Scala 3:
164
+
165
+
```scala
166
+
traitMonad[F[?]] extendsFunctor[F] { // "A `Monad` for type `F[?]` is a `Functor[F]`" => thus has the `map` ability
167
+
defpure[A](x: A):F[A] // `pure` can construct F[A] from a single value A
168
+
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
169
+
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
51
170
}
171
+
```
172
+
173
+
#### List
52
174
175
+
Let us declare the `Monad` ability for type `List`
xs.flatMap(f) // let's rely on the existing `flatMap` method of `Option`
200
+
}
201
+
```
202
+
203
+
#### The Reader Monad
204
+
205
+
Another example of a `Monad` is the Reader Monad. It no longer acts on a type like `List` or `Option`, but on a function.
206
+
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.
207
+
208
+
Let us have a `Config` type, and two functions using it:
209
+
210
+
```scala
211
+
traitConfig
212
+
// ...
213
+
defcompute(i: Int)(config: Config):String=???
214
+
defshow(str: String)(config: Config):Unit=???
215
+
```
216
+
217
+
We may want to combine `compute` and `show` into a single function, accepting a `Config` as parameter, and showing the result of the computation.
218
+
If we had a `flatMap` function as in the examples above, we would be able to write the following:
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`.
225
+
226
+
```scala
227
+
traitConfig// the Config defined above
228
+
typeConfigDependent[Result] =Config=>Result
229
+
```
230
+
231
+
The monad will look like this:
232
+
233
+
```scala
234
+
givenconfigDependentMonad as Monad[ConfigDependent] {
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:
The definition of a _typeclass_ is expressed via a parameterised type with abstract members, such as a `trait`.
275
+
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.
276
+
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.
277
+
278
+
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_.
0 commit comments