Skip to content

Commit bc42300

Browse files
committed
[docs] Add a blog post about type classes
1 parent 8148028 commit bc42300

File tree

2 files changed

+289
-0
lines changed

2 files changed

+289
-0
lines changed
Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
---
2+
authors:
3+
- jackkoenig
4+
tags: [design patterns]
5+
slug: arbiter-type-class
6+
description: Type classes and hardware design.
7+
---
8+
9+
# Type Classes and Hardware Design
10+
11+
When giving talks about Chisel, I always say that Chisel is intended for writing _reusable hardware generators_.
12+
The thesis is that, by virtue of being embedded in Scala, Chisel can take advantage of powerful programming language features to make hardware design easier.
13+
How to actually do this; however, is usually left as an exercise for the audience.
14+
15+
In this blog post, we'll explore how one might use the *type class* pattern to build Chisel generators.
16+
17+
## A Specialized Arbiter for a Specific Protocol
18+
19+
Let's first build a simple fixed-priority arbiter that works for a specific kind of client:
20+
21+
```scala
22+
class ArbiterClient extends Bundle {
23+
val request = Bool()
24+
val grant = Flipped(Bool())
25+
}
26+
27+
class PriorityArbiter(nClients: Int) extends Module {
28+
val clients = IO(Vec(nClients, Flipped(new ArbiterClient)))
29+
30+
val requests = clients.map(_.request)
31+
val granted = PriorityEncoderOH(requests)
32+
33+
for ((client, grant) <- clients.zip(granted)) {
34+
client.grant := grant
35+
}
36+
}
37+
```
38+
39+
<!-- truncate -->
40+
41+
`ArbiterClient` is written from the perspective of the client--`request` is in the default orientation, intended to be driven by the client, and `grant` is flipped, intended to be driven by the arbiter.
42+
43+
This works fine if you only ever need to arbitrate `ArbiterClient` bundles, but it doesn't generalize.
44+
For example, how would you use this arbiter with a `ready-valid` interface like `Decoupled`?
45+
46+
```scala
47+
val needToArbitrate = IO(Vec(4, Flipped(Decoupled(UInt(8.W)))))
48+
val arbiter = Module(new PriorityArbiter(needToArbitrate.size))
49+
50+
for ((client, decoupled) <- arbiter.clients.zip(needToArbitrate)) {
51+
client.request := decoupled.valid
52+
decoupled.ready := client.grant
53+
}
54+
```
55+
56+
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.
58+
59+
## Generalizing the Arbiter
60+
61+
What we really want is an arbiter that is generic to the type of the client.
62+
For those familiar with _Object-Oriented Programming_, you might be tempted to try to use inheritance to define a common interface for clients:
63+
64+
```scala
65+
trait Arbitrable {
66+
def request: Bool
67+
def grant: Bool
68+
}
69+
70+
class ArbiterClient extends Bundle with Arbitrable {
71+
val request = Bool()
72+
val grant = Flipped(Bool())
73+
}
74+
```
75+
76+
However, for our example above, this would be a bit difficult.
77+
`Decoupled` is defined within Chisel itself--how can a user make Chisel's `Decoupled` inherit from `Arbitrable`?
78+
79+
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.
81+
82+
```scala
83+
class GenericPriorityArbiter[A <: Data](
84+
nClients: Int,
85+
clientType: A
86+
)(
87+
requestFn: A => Bool,
88+
grantFn: (A, Bool) => Unit) extends Module {
89+
val clients = IO(Vec(nClients, Flipped(clientType)))
90+
91+
val requests = clients.map(requestFn(_))
92+
val granted = PriorityEncoderOH(requests)
93+
94+
for ((client, grant) <- clients.zip(granted)) {
95+
grantFn(client, grant)
96+
}
97+
}
98+
```
99+
100+
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.
102+
103+
Now we can use it for both our `ArbiterClient` and `Decoupled` interfaces.
104+
105+
```scala
106+
val clients1 = IO(Vec(4, Flipped(new ArbiterClient)))
107+
val arbiter1 = Module(
108+
new GenericPriorityArbiter(4, new ArbiterClient)(_.request, (c, g) => c.grant := g)
109+
)
110+
arbiter1.clients :<>= clients1
111+
112+
val clients2 = IO(Vec(4, Flipped(Decoupled(UInt(8.W)))))
113+
val arbiter2 = Module(
114+
new GenericPriorityArbiter(4, Decoupled(UInt(8.W)))(_.valid, (d, g) => d.ready := g)
115+
)
116+
arbiter2.clients :<>= clients2
117+
```
118+
119+
This is still a bit clunky--we have to pass two additional arguments to the arbiter.
120+
121+
Another thing to consider is that these additional arguments are the same for all instances of a given type.
122+
For example, if we were to arbitrate another set of `Decoupled` interfaces, we would have to pass the same functions again:
123+
124+
```scala
125+
val clients3 = IO(Vec(4, Flipped(Decoupled(UInt(8.W)))))
126+
val arbiter3 = Module(
127+
new GenericPriorityArbiter(4, Flipped(Decoupled(UInt(8.W))))(_.valid, (d, g) => d.ready := g)
128+
) // the two function arguments are the same as above.
129+
arbiter2.clients :<>= clients3
130+
```
131+
132+
## Introducing a Type Class
133+
134+
To clean this up even more, we can introduce a type class that captures the "arbitrable" pattern:
135+
136+
```scala
137+
trait Arbitrable[A] {
138+
def request(a: A): Bool
139+
def grant(a: A, value: Bool): Unit
140+
}
141+
```
142+
143+
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.
145+
146+
We can then provide instances of this type class for specific types. For example, for `ArbiterClient` and `Decoupled`:
147+
148+
```scala
149+
class ArbiterClientArbitrable extends Arbitrable[ArbiterClient] {
150+
def request(a: ArbiterClient) = a.request
151+
def grant(a: ArbiterClient, value: Bool) = a.grant := value
152+
}
153+
154+
class DecoupledArbitrable[T <: Data] extends Arbitrable[DecoupledIO[T]] {
155+
def request(a: DecoupledIO[T]) = a.valid
156+
def grant(a: DecoupledIO[T], value: Bool) = a.ready := value
157+
}
158+
```
159+
160+
Then, we can refactor the arbiter to use the type class:
161+
162+
```scala
163+
class GenericPriorityArbiter[A <: Data](nClients: Int, clientType: A, arbitrable: Arbitrable[A]) extends Module {
164+
val clients = IO(Vec(nClients, Flipped(clientType)))
165+
166+
val requests = clients.map(arbitrable.request(_))
167+
val granted = PriorityEncoderOH(requests)
168+
169+
for ((client, grant) <- clients.zip(granted)) {
170+
arbitrable.grant(client, grant)
171+
}
172+
}
173+
```
174+
175+
This makes instantiating the arbiter a hair nicer:
176+
177+
```scala
178+
val clients1 = IO(Vec(4, Flipped(new ArbiterClient)))
179+
val arbiter1 = Module(new GenericPriorityArbiter(4, new ArbiterClient, new ArbiterClientArbitrable))
180+
arbiter1.clients :<>= clients1
181+
182+
val clients2 = IO(Vec(4, Flipped(Decoupled(UInt(8.W)))))
183+
val arbiter2 = Module(new GenericPriorityArbiter(4, Decoupled(UInt(8.W)), new DecoupledArbitrable[UInt]))
184+
arbiter2.clients :<>= clients2
185+
```
186+
187+
At least we aren't repeating logic anymore, instead we get to just refer to the type class instance.
188+
189+
However, we can do even better.
190+
191+
## Implicit Type Class Instances
192+
193+
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+
197+
Let us rewrite our typeclass instances as implicit values:
198+
199+
```scala
200+
implicit val arbiterClientArbitrable: Arbitrable[ArbiterClient] =
201+
new Arbitrable[ArbiterClient] {
202+
def request(a: ArbiterClient) = a.request
203+
def grant(a: ArbiterClient, value: Bool) = a.grant := value
204+
}
205+
206+
// 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.
208+
implicit def decoupledArbitrable[T <: Data]: Arbitrable[DecoupledIO[T]] =
209+
new Arbitrable[DecoupledIO[T]] {
210+
def request(a: DecoupledIO[T]) = a.valid
211+
def grant(a: DecoupledIO[T], value: Bool) = a.ready := value
212+
}
213+
```
214+
215+
And then we can refactor the arbiter to use the implicit type class instance:
216+
217+
```scala
218+
class GenericPriorityArbiter[A <: Data](nClients: Int, clientType: A)(implicit arbitrable: Arbitrable[A]) extends Module {
219+
val clients = IO(Vec(nClients, Flipped(clientType)))
220+
221+
val requests = clients.map(arbitrable.request(_))
222+
val granted = PriorityEncoderOH(requests)
223+
224+
for ((client, grant) <- clients.zip(granted)) {
225+
arbitrable.grant(client, grant)
226+
}
227+
}
228+
```
229+
230+
Now, we can instantiate the arbiter without passing the type class instance explicitly:
231+
232+
```scala
233+
val clients1 = IO(Vec(4, Flipped(new ArbiterClient)))
234+
val arbiter1 = Module(new GenericPriorityArbiter(4, new ArbiterClient))
235+
arbiter1.clients :<>= clients1
236+
237+
val clients2 = IO(Vec(4, Flipped(Decoupled(UInt(8.W)))))
238+
val arbiter2 = Module(new GenericPriorityArbiter(4, Decoupled(UInt(8.W))))
239+
arbiter2.clients :<>= clients2
240+
```
241+
242+
This is much cleaner and more readable.
243+
244+
Scala also has special syntax for implicit typeclass instances:
245+
246+
```scala
247+
class GenericPriorityArbiter[A <: Data : Arbitrable](nClients: Int, clientType: A) extends Module {
248+
...
249+
}
250+
```
251+
252+
This is equivalent to the previous definition, but is more concise.
253+
Note that unlike the version with the implicit argument, this one does not bind a variable name for the implicit argument.
254+
255+
In the body of `GenericPriorityArbiter`, we can get a reference to the implicity value by calling `implicitly[Arbitrable[A]]`:
256+
```scala
257+
val arbitrable = implicitly[Arbitrable[A]]
258+
```
259+
260+
Note that Scala has rules for _implicit resolution_ for how to find the type class instance for a given type.
261+
As a general rule, you should define implicit type class instances in the companion object of the type they are for, or in the companion object for the type class itself.
262+
263+
For example, since `DecoupledIO` is defined in Chisel itself, you could define the implicit value in the companion object for `Arbitrable`:
264+
```scala
265+
object Arbitrable {
266+
implicit def decoupledArbitrable[T <: Data]: Arbitrable[DecoupledIO[T]] = ...
267+
}
268+
```
269+
270+
For more information, see [further reading](#further-reading) below.
271+
272+
## Conclusion
273+
274+
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.
276+
277+
### Further Reading
278+
279+
* Official Scala [documentation about type classes](https://docs.scala-lang.org/scala3/book/ca-type-classes.html)--make sure to click on the `Scala 2` tab since Chisel only currently supports Scala 2.
280+
* Chisel DataView explanation's [section on Type Classes](../../docs/explanations/dataview#type-classes). In particular, check out the section on [implicit resolution](../../docs/explanations/dataview#implicit-resolution).

website/blog/authors.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,12 @@ seldridge:
88
x: theSchuyler
99
github: seldridge
1010
linkedin: schuylereldridge
11+
jackkoenig:
12+
name: Jack Koenig
13+
title: Senior Staff Engineer at SiFive
14+
image_url: https://github.com/jackkoenig.png
15+
page: true
16+
socials:
17+
x: jackakattack
18+
github: jackkoenig
19+
linkedin: koenigjack

0 commit comments

Comments
 (0)