Skip to content

Commit 43bbaaa

Browse files
jackkoenigmwachs5
andcommitted
Apply suggestions from code review
Co-authored-by: Megan Wachs <[email protected]>
1 parent 82b7495 commit 43bbaaa

File tree

1 file changed

+22
-15
lines changed
  • website/blog/2025/05-05-arbiter-type-class

1 file changed

+22
-15
lines changed

website/blog/2025/05-05-arbiter-type-class/index.md

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ for ((client, decoupled) <- arbiter.clients.zip(needToArbitrate)) {
5454
```
5555

5656
This works, but its a bit clunky.
57-
Considering the implementation of `PriorityArbiter`, is only 6 lines of Scala, it's not ideal that it takes another 4 lines to use it.
57+
Considering the implementation of `PriorityArbiter` is only 6 lines of Scala, it's not ideal that it takes another 4 lines to use it.
5858

5959
## Generalizing the Arbiter
6060

@@ -77,14 +77,16 @@ However, for our example above, this would be a bit difficult.
7777
`Decoupled` is defined within Chisel itself--how can a user make Chisel's `Decoupled` inherit from `Arbitrable`?
7878

7979
Instead, we can try something different.
80-
We could make the Arbiter generic to the type of the client and then use higher-order functions to extract the request and grant signals.
80+
We could make the Arbiter generic to the type of the client and then require additional function arguments that tell how extract the request and grant signals from the particular client type we are using.
8181

8282
```scala
8383
class GenericPriorityArbiter[A <: Data](
8484
nClients: Int,
8585
clientType: A
8686
)(
87+
/** Function that indicates how to connect request from type A */
8788
requestFn: A => Bool,
89+
/** Function that indicates how to connect the grant from type A */
8890
grantFn: (A, Bool) => Unit) extends Module {
8991
val clients = IO(Vec(nClients, Flipped(clientType)))
9092

@@ -98,7 +100,7 @@ class GenericPriorityArbiter[A <: Data](
98100
```
99101

100102
You may notice this looks quite similar to the original `PriorityArbiter` in its implementation.
101-
It uses two parameter lists in order to help the Scala type inferencer derive the types of the functions of the type of the client--we could do this with one parameter list but then it would require explicitly passing the type of the client.
103+
> NOTE: It uses two parameter lists in order to help the Scala type inferencer derive the types of the functions of the type of the client--we could do this with one parameter list but then it would require explicitly passing the type of the client.
102104
103105
Now we can use it for both our `ArbiterClient` and `Decoupled` interfaces.
104106

@@ -131,7 +133,7 @@ arbiter2.clients :<>= clients3
131133

132134
## Introducing a Type Class
133135

134-
To clean this up even more, we can introduce a type class that captures the "arbitrable" pattern:
136+
To clean this up even more, we can introduce a _type class_ that captures the "arbitrable" pattern:
135137

136138
```scala
137139
trait Arbitrable[A] {
@@ -141,7 +143,7 @@ trait Arbitrable[A] {
141143
```
142144

143145
Effectively, we have taken the two arguments to the arbiter and turned them into methods on the type class.
144-
This looks similar to the proposed object-oriented version of `Arbitrable` above, but note how it is parameterized by the type of the client and accepts the client as an argument.
146+
This looks similar to the proposed object-oriented version of `Arbitrable` above, but note how it is parameterized by the type of the client and each function defined in the trait accepts the client as an argument.
145147

146148
We can then provide instances of this type class for specific types. For example, for `ArbiterClient` and `Decoupled`:
147149

@@ -157,7 +159,7 @@ class DecoupledArbitrable[T <: Data] extends Arbitrable[DecoupledIO[T]] {
157159
}
158160
```
159161

160-
Then, we can refactor the arbiter to use the type class:
162+
Then, we can refactor the arbiter to use the type class so that we're getting one 'package' of functions for type A, not having to pass them individually:
161163

162164
```scala
163165
class GenericPriorityArbiter[A <: Data](nClients: Int, clientType: A, arbitrable: Arbitrable[A]) extends Module {
@@ -184,35 +186,37 @@ val arbiter2 = Module(new GenericPriorityArbiter(4, Decoupled(UInt(8.W)), new De
184186
arbiter2.clients :<>= clients2
185187
```
186188

187-
At least we aren't repeating logic anymore, instead we get to just refer to the type class instance.
189+
At least we aren't repeating logic anymore, instead we get to reuse the code for making the type class.
188190

189191
However, we can do even better.
190192

191193
## Implicit Type Class Instances
192194

193195
Scala has a powerful feature called **implicit resolution**.
194-
This allows us to avoid passing around the type class instance explicitly.
195-
Instead, we can define the type class instance as an implicit value and the compiler will automatically find it for us.
196+
This allows us to avoid figuring out what type class we need to instantiate at every call site.
197+
Instead, we can define a default function to use when a specific type class is needed, and the compiler will automatically find it for us. We do this by making the argument to the function implicit, then making sure the implicit value of the type class is in scope.
196198

197-
Let us rewrite our typeclass instances as implicit values:
199+
Let us instantiate implicit functions to create our type class instances. This tells the compiler, "if you need a function to create an `Arbitrable[ArbiterClient]`, use this one."
198200

199201
```scala
202+
// We could make a def, but since this function is the same every time, we just make this a `val`.
200203
implicit val arbiterClientArbitrable: Arbitrable[ArbiterClient] =
201204
new Arbitrable[ArbiterClient] {
202205
def request(a: ArbiterClient) = a.request
203206
def grant(a: ArbiterClient, value: Bool) = a.grant := value
204207
}
205208

206209
// In chisel3.util, the type is DecoupledIO while we construct instances of it with Decoupled.
207-
// Note that this is a def because DecoupledIO itself takes a type parameter.
210+
// Note that this is a def because DecoupledIO itself takes a type parameter,
211+
// so we can't reuse the same one for every call-site.
208212
implicit def decoupledArbitrable[T <: Data]: Arbitrable[DecoupledIO[T]] =
209213
new Arbitrable[DecoupledIO[T]] {
210214
def request(a: DecoupledIO[T]) = a.valid
211215
def grant(a: DecoupledIO[T], value: Bool) = a.ready := value
212216
}
213217
```
214218

215-
And then we can refactor the arbiter to use the implicit type class instance:
219+
Now we can refactor the arbiter to make its `arbitrable` argument `implicit`:
216220

217221
```scala
218222
class GenericPriorityArbiter[A <: Data](nClients: Int, clientType: A)(implicit arbitrable: Arbitrable[A]) extends Module {
@@ -239,11 +243,14 @@ val arbiter2 = Module(new GenericPriorityArbiter(4, Decoupled(UInt(8.W))))
239243
arbiter2.clients :<>= clients2
240244
```
241245

242-
This is much cleaner and more readable.
246+
This is much cleaner and more readable. Even more importantly, it makes it the responsibility of the library
247+
writer to determine how to make a certain type `Arbitrable`, not everyone who instantiates an arbiter.
243248

244-
Scala also has special syntax for implicit typeclass instances:
249+
Scala also has special syntax for the second, implicit argument list:
245250

246251
```scala
252+
// Equivalent to:
253+
// class GenericPriorityArbiter[A <: Data](nClients: Int, clientType: A)(implicit arbitrable: Arbitrable[A]) extends Module {
247254
class GenericPriorityArbiter[A <: Data : Arbitrable](nClients: Int, clientType: A) extends Module {
248255
...
249256
}
@@ -272,7 +279,7 @@ For more information, see [further reading](#further-reading) below.
272279
## Conclusion
273280

274281
This example only scratches the surface of what type classes can do in Chisel and Scala.
275-
Whenever you find yourself passing around the same bits of logic repeatedly, think about whether a type class could capture that pattern.
282+
Whenever you find yourself passing functions around repeatedly, or are struggling with an inheritance pattern, think about whether a type class could capture that pattern.
276283

277284
### Further Reading
278285

0 commit comments

Comments
 (0)