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
Copy file name to clipboardExpand all lines: docs/docs/reference/contextual/type-classes.md
+16-7Lines changed: 16 additions & 7 deletions
Original file line number
Diff line number
Diff line change
@@ -4,13 +4,14 @@ title: "Implementing Type classes"
4
4
---
5
5
6
6
A _type class_ 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
+
7
8
* expressing how a type you don't own (from the standard or 3rd-party library) conforms to such behavior
8
9
* 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
10
11
Therefore in Scala 3, _type classes_ are just _traits_ with one or more parameters whose implementations are not defined through the `extends` keyword, but by **given instances**.
11
12
Here are some examples of common type classes:
12
13
13
-
### Semigroups and monoids:
14
+
### Semigroups and monoids
14
15
15
16
Here's the `Monoid` type class definition:
16
17
@@ -31,6 +32,7 @@ given Monoid[String]:
31
32
```
32
33
33
34
Whereas for the type `Int` one could write the following:
35
+
34
36
```scala
35
37
givenMonoid[Int]:
36
38
extension (x: Int) defcombine (y: Int):Int= x + y
@@ -58,7 +60,7 @@ def combineAll[T: Monoid](xs: List[T]): T =
58
60
xs.foldLeft(Monoid[T].unit)(_.combine(_))
59
61
```
60
62
61
-
### Functors:
63
+
### Functors
62
64
63
65
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.
64
66
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.
@@ -69,6 +71,7 @@ The definition of a generic `Functor` would thus be written as:
69
71
traitFunctor[F[_]]:
70
72
defmap[A, B](x: F[A], f: A=>B):F[B]
71
73
```
74
+
72
75
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 function `f` with type `A => B`". We call the `Functor` definition here a _type class_.
73
76
This way, we could define an instance of `Functor` for the `List` type:
74
77
@@ -81,6 +84,7 @@ given Functor[List]:
81
84
With this `given` instance in scope, everywhere a `Functor` is expected, the compiler will accept a `List` to be used.
82
85
83
86
For instance, we may write such a testing method:
87
+
84
88
```scala
85
89
defassertTransformation[F[_]:Functor, A, B](expected: F[B], original: F[A], mapping: A=>B):Unit=
@@ -117,18 +121,18 @@ It simplifies the `assertTransformation` method:
117
121
defassertTransformation[F[_]:Functor, A, B](expected: F[B], original: F[A], mapping: A=>B):Unit=
118
122
assert(expected == original.map(mapping))
119
123
```
124
+
120
125
The `map` method is now directly used on `original`. It is available as an extension method
121
126
since `original`'s type is `F[A]` and a given instance for `Functor[F[A]]` which defines `map`
122
127
is in scope.
123
128
124
-
125
129
### Monads
126
130
127
131
Applying `map` in `Functor[List]` to a mapping function of type `A => B` results in a `List[B]`. So applying it to a mapping function of type `A => List[B]` results in a `List[List[B]]`. To avoid managing lists of lists, we may want to "flatten" the values in a single list.
128
132
129
133
That's where `Monad` comes in. A `Monad` for type `F[_]` is a `Functor[F]` with two more operations:
130
-
*`flatMap`, which turns an `F[A]` into an `F[B]` when given a function of type
131
-
`A => F[B]`,
134
+
135
+
*`flatMap`, which turns an `F[A]` into an `F[B]` when given a function of type `A => F[B]`,
132
136
*`pure`, which creates an `F[A]` from a single value `A`.
133
137
134
138
Here is the translation of this definition in Scala 3:
@@ -152,6 +156,7 @@ end Monad
152
156
#### List
153
157
154
158
A `List` can be turned into a monad via this `given` instance:
159
+
155
160
```scala
156
161
givenlistMonad as Monad[List]:
157
162
defpure[A](x: A):List[A] =
@@ -160,6 +165,7 @@ given listMonad as Monad[List]:
160
165
defflatMap(f: A=>List[B]):List[B] =
161
166
xs.flatMap(f) // rely on the existing `flatMap` method of `List`
162
167
```
168
+
163
169
Since `Monad` is a subtype of `Functor`, `List` is also a functor. The Functor's `map`
164
170
operation is already provided by the `Monad` trait, so the instance does not need to define
We may want to combine `compute` and `show` into a single function, accepting a `Config` as parameter, and showing the result of the computation, and we'd like to use
197
203
a monad to avoid passing the parameter explicitly multiple times. So postulating
Let's define this m then. First, we are going to define a type named `ConfigDependent` representing a function that when passed a `Config` produces a `Result`.
207
217
208
218
```scala
@@ -232,7 +242,6 @@ type ConfigDependent = [Result] =>> Config => Result
232
242
233
243
Using this syntax would turn the previous `configDependentMonad` into:
234
244
235
-
236
245
```scala
237
246
givenconfigDependentMonad as Monad[[Result] =>>Config=>Result]
238
247
@@ -266,7 +275,7 @@ end readerMonad
266
275
The definition of a _type class_ is expressed with a parameterised type with abstract members, such as a `trait`.
267
276
The main difference between subtype polymorphism and ad-hoc polymorphism with _type classes_ is how the definition of the _type class_ is implemented, in relation to the type it acts upon.
268
277
In the case of a _type class_, its implementation for a concrete type is expressed through a `given` instance definition, which is supplied as an implicit argument alongside the value it acts upon. With subtype polymorphism, the implementation is mixed into the parents of a class, and only a single term is required to perform a polymorphic operation. The type class solution
269
-
takes more effort to set up, but is more extensible: Adding a new interface to a class
278
+
takes more effort to set up, but is more extensible: Adding a new interface to a
270
279
class requires changing the source code of that class. But contrast, instances for type classes can be defined anywhere.
271
280
272
281
To conclude, we have seen that traits and given instances, combined with other constructs like extension methods, context bounds and type lambdas allow a concise and natural expression of _type classes_.
0 commit comments