(完结篇)Scala和Chisel中等数据类型
结束开头的碎念
这是这个系列的最后一篇文章,官方的Chisel-Bootcamp中后面还有FIRRTL相关内容,但设计一个RISC-V CPU这样的目标,依靠本系列文章的内容就足够了。因为这个系列的学习倾向于在实战中学习,难免会有很多遗漏。所以后续会更新一个系列,争取对抗Chisel全面详细地讲解语言。
另外,除了Chisel语言的基础学习也会像以前承诺的那样针对RISC-V系统结构的实现是一个系列。该系列将从只有几个指令的单周期开始CPU逐步实现支持RV64GC复杂的指令集、可操作系统CPU。敬请期待!
目前国内研究体系结构较少,从事或计划相关工作的人不多,各大知识共享平台上的相关文章也不热门,,毕竟冷门研究方向啊。本人为某计科被评为A 硕士期间研究课题与计算机系统结构交集不多,但对系统结构有浓厚的兴趣。所以毕业后一直在考虑继续学习系统结构,所以有了这个系列。
到目前为止,这些文章很少被浏览,但它们总是需要的。在这种情况下,我会继续做这些事情。
动机
言归正传,Scala这是一种强大的编程语言,这是一把双刃剑:坏的方面,很多都可以Python解释和执行的程序在上面Scala中连的编译不能通过,因为Python是动态语言;好的方面,Scala通过编译的程序在运行时会比较Python少犯错误。
这篇文章的目的是让我们更好地理解它Scala中等公民的数据类型(types)。一开始,我们可能会觉得我们写代码的效率很低,但很快,我们就会学会理解编译中的错误信息,并进一步理解如何使用类型系统来构建我们的程序。
静态类型
Scala中的类型
Scala所有对象都有一种类型,通常是这个对象的类型,例如:
println(10.getClass) println(10.0.getClass) println("ten".getClass)
输出为:
int double class java.lang.String
当我们声明自己的类别时,它也有相应的类型:
class MyClass {
val a = 1 } println(new MyClass().getClass)
输出为:
defined class MyClass
若无要求,。这会让Scala编译器捕获函数使用不当。
def double(s: String): String = s s // 这些句子可以试试 double("hi") // 使用正确,会返回"hihi" double(10) // 编译器会报告错误用法的参数类型 double("hi") / 10 // 使用错误的用法,编译器错误地使用返回值(/ 不是String的成员)
如果函数不返回任何值,则返回类型为Unit
。
var counter = 0 def increment(): Unit = {
counter = 1 } increment()
Scala类型 vs. Chisel类型
我们前面讨论过Chisel类型和Scala例如:
val a = Wire(UInt(4.W))
a := 0.U
这个是正确用法,因为0.U
是UInt
类型(一个Chisel类型)的,而:
val a = Wire(UInt(4.W))
a := 0
就不对,因为0
是Int
类型(一个Scala类型)的。
还有布尔值,在Scala里面是Boolean
,而在Chisel里面是Bool
:
val bool = Wire(Bool())
val boolean: Boolean = false
// 正确用法
when (bool) {
... }
if (boolean) {
... }
// 错误用法
if (bool) {
... }
when (boolean) {
... }
如果我们不小心弄混了Scala类型和Chisel类型,Scala编译器会为我们生成捕获到的错误。这是因为Scala是静态类型的,在编译的时候,编译器能够区分Chisel和Scala类型,还能理解if ()
里面应该是Boolean
而when ()
里面应该是Bool
。
Scala强制类型转换(asInstanceOf
)
x.asInstanceOf[T]
可以将对象x
转换为类型T
,如果不能够完成到T
类型的转换就会抛出一个异常:
val x: UInt = 3.U
try {
println(x.asInstanceOf[Int]) // 会抛出异常
} catch {
case e: java.lang.ClassCastException => println("As expected, we can't cast UInt to Int")
}
// 但是我们可以将x强制转换为Data类型,因为UInt就是从Data继承的
println(x.asInstanceOf[Data])
输出为:
As expected, we can't cast UInt to Int
UInt<2>(3)
Chisel中的类型转换
下面的代码运行的话Scala内核会直接挂掉(为什么不是报错呢?不知道),因为将UInt
类型的值赋值给了SInt
,这是非法的:
class TypeConvertDemo extends Module {
val io = IO(new Bundle {
val in = Input(UInt(4.W))
val out = Output(SInt(4.W))
})
io.out := io.in
}
test(new TypeConvertDemo) {
c =>
c.io.in.poke(3.U)
c.io.out.expect(3.S)
c.io.in.poke(15.U)
c.io.out.expect(-1.S)
}
Chisel有一组类型转换函数,其中最常用的强制类型转换用的是x.asTypeOf(T)
,如果把第6行代码改成:
io.out := io.in.asTypeOf(io.out)
问题就解决了,测试通过。有一些Chisel对象还定义了asUInt()
和asSInt()
。
类型匹配
匹配操作符
匹配操作符match
在前面就已经讲过。类型匹配在写泛型生成器(Type-generic Generator)的时候尤其有用。下面是一个生成器的例子,它可以把两个UInt
或SInt
的字面值相加。后面还会继续对泛型生成器进行阐述。
下面的例子中,chiselTypeOf
可以用于获取字面值的Chisel类型。注意:Scala中其实有更好、更安全的办法来写泛型生成器的。
class ConstantSum(in1: Data, in2: Data) extends Module {
val io = IO(new Bundle {
val out = Output(chiselTypeOf(in1)) // 获取in1的类型
})
(in1, in2) match {
case (x: UInt, y: UInt) => io.out := x + y
case (x: SInt, y: SInt) => io.out := x + y
case _ => throw new Exception("I give up!")
}
}
println(getVerilogString(new ConstantSum(3.U, 4.U)))
println(getVerilogString(new ConstantSum(-3.S, 4.S)))
println(getVerilogString(new ConstantSum(3.U, 4.S))) // 会触发异常
输出如下:
module ConstantSum(
input clock,
input reset,
output [1:0] io_out
);
assign io_out = 2'h3; // @[MyModule.scala 15:43]
endmodule
module ConstantSum(
input clock,
input reset,
output [2:0] io_out
);
assign io_out = 3'sh1; // @[MyModule.scala 16:43]
endmodule
[error] java.lang.Exception: I give up!
需要记住的是,Chisel类型通常不应该进行值匹配。Scala的match
会在电路展开时进行,但是你可能想要的是展开后的比较。下面的例子就会报一个符号错误:
class InputIsZero extends Module {
val io = IO(new Bundle {
val in = Input(UInt(16.W))
val out = Output(Bool())
})
io.out := (io.in match {
// 这里会触发编译报语法错误
case (0.U) => true.B
case _ => false.B
})
}
println(getVerilogString(new InputIsZero))
Unapply
当进行匹配的时候原理是什么呢?为什么我们可以完成下面这样有意思的匹配呢:
case class Something(a: String, b: Int)
val a = Something("A", 3)
a match {
case Something("A", value) => value
case Something(str, 3) => 0
}
这是因为每个case类都会有个伴生对象被创建,其中包含了一个unapply
方法,作为apply
方法的补充。那什么是unapply
方法呢?
Scala中的unapply
方法是另一种语法糖的形式,它可以给匹配语句在匹配中既能匹配类型,也能从类型中的能力。
来看下面这个例子。如果我们给参数说这个生成器流水线化了,那延迟就是3*totalWidth
,否则就是2*someOtherWidth
。因为case类有定义了的unapply
,所以我们可以在case类里进行值匹配,就像这样:
case class SomeGeneratorParameters(
someWidth: Int,
someOtherWidth: Int = 10,
pipelineMe: Boolean = false
) {
require(someWidth >= 0)
require(someOtherWidth >= 0)
val totalWidth = someWidth + someOtherWidth
}
def delay(p: SomeGeneratorParameters): Int = p match {
case sg @ SomeGeneratorParameters(_, _, true) => sg.totalWidth * 3
case SomeGeneratorParameters(_, sw, false) => sw * 2
}
println(delay(SomeGeneratorParameters(10, 10)))
println(delay(SomeGeneratorParameters(10, 10, true)))
输出为:
20
60
如果仔细看delay
函数的话,应该可以注意到除了进行类型匹配以外,我们还:
- 直接引用了参数的内部值;
- 直接在参数的内部值上进行比较;
这是编译器实现了unapply
方法给予的可能性。要注意的是case类的unapply
只是个语法糖,下面这两条语句是等价的:
case p: SomeGeneratorParameters => p.sw * 2
case SomeGeneratorParameters(_, sw, _) => sw * 2
此外,匹配还有其他的语法和风格。下面这两条语句也是等价的,但是第二条语句允许在内部值上进行匹配的同时还引用父值:
case SomeGeneratorParameters(_, sw, true) => sw
case sg@SomeGeneratorParameters(_, sw, true) => sw
我们还可以直接在检查语句中嵌入条件检查,就像下面三条等价的语句演示的一样:
case SomeGeneratorParameters(_, sw, false) => sw * 2
case s@SomeGeneratorParameters(_, sw, false) => s.sw * 2
case s: SomeGeneratorParameters if s.pipelineMe => s.sw * 2
上面所有的语法能用都是因为Scala类的伴生对象中的unapply
方法。如果想unapply
一个类但又不想把它写成case类,那可以手动实现一个unapply
方法。下面的例子就演示了如何手动实现一个类的apply
和unapply
方法:
class Boat(val name: String, val length: Int)
object Boat {
def unapply(b: Boat): Option[(String, Int)] = Some((b.name, b.length))
def apply(name: String, length: Int): Boat = new Boat(name, length)
}
def getSmallBoats(seq: Seq[Boat]): Seq[Boat] = seq.filter {
b =>
b match {
case Boat(_, length) if length < 60 => true
case Boat(_, _) => false
}
}
val boats = Seq(Boat("Santa Maria", 62), Boat("Pinta", 56), Boat("Nina", 50))
println(getSmallBoats(boats).map(_.name).mkString(" and ") + " are small boats!")
输出为:
Pinta and Nina are small boats!
偏函数(部分应用函数)
这里只是简单介绍一下,详细的可以自己去找Scala的文档。
偏函数(PartialFunction
)是一种只定义在它输入的一个子集上的函数。就跟一个选项一样,偏函数也许对于特定输入不会返回值,这个可以通过isDefinedAt(...)
来测试。
偏函数之间可以通过orElse
串起来。注意,用未定义的输入调用偏函数会导致运行时错误。比如在给偏函数的输入是用户定义的时候就会发生。所以为了更具类型安全,建议写函数的时候返回一个Option
。下面看例子,最后一段的partialFunc3
通过or
把两个偏函数的定义域取并集了:
// 用来打印的助手函数,不然代码太冗长了
def printAndAssert(cmd: String, result: Boolean, expected: Boolean): Unit = {
println(s"$cmd = $result")
assert(result == expected)
}
// 这个偏函数是为..., -1, 2, 5, ...这些数定义的
val partialFunc1: PartialFunction[Int, String] = {
case i if (i + 1) % 3 == 0 => "Something"
}
// 在定义内的
printAndAssert("partialFunc1.isDefinedAt(2)", partialFunc1.isDefinedAt(2), true)
printAndAssert("partialFunc1.isDefinedAt(5)", partialFunc1.isDefinedAt(5), true)
// 不在定义内的
printAndAssert("partialFunc1.isDefinedAt(1)", partialFunc1.isDefinedAt(1), false)
printAndAssert("partialFunc1.isDefinedAt(0)", partialFunc1.isDefinedAt(0), false)
println(s"partialFunc1(2) = ${
partialFunc1(2)}") // 可以执行
try {
println(partialFunc1(0)) // 抛出异常
} catch {
case e: scala.MatchError => println("partialFunc1(0) = can't apply PartialFunctions where they are not defined")
}
// 这个偏函数是为..., 1, 4, 7, ...这些数定义的
val partialFunc2: PartialFunction[Int, String] = {
case i if (i + 2) % 3 == 0 => "Something else"
}
// 在定义内的
printAndAssert("partialFunc2.isDefinedAt(1)", partialFunc2.isDefinedAt(1), true)
// 不在定义内的
printAndAssert("partialFunc2.isDefinedAt(0)", partialFunc2.isDefinedAt(0), false)
println(s"partialFunc2(1) = ${
partialFunc2(1)}") // 可以执行
try {
println(partialFunc2(0)) // 抛出异常
} catch {
case e: scala.MatchError => println("partialFunc2(0) = can't apply PartialFunctions where they are not defined")
}
val partialFunc3 = partialFunc1 orElse partialFunc2 // 定义域取并集
// 不在定义内的
printAndAssert("partialFunc3.isDefinedAt(0)", partialFunc3.isDefinedAt(0), false)
// 在定义内的
printAndAssert("partialFunc3.isDefinedAt(1)", partialFunc3.isDefinedAt(1), true)
printAndAssert("partialFunc3.isDefinedAt(2)", partialFunc3.isDefinedAt(2), true)
// 不在定义内的
printAndAssert("partialFunc3.isDefinedAt(3)", partialFunc3.isDefinedAt(3), false)
println(s"partialFunc3(1) = ${
partialFunc3(1)}")
println(s"partialFunc3(2) = ${
partialFunc3(2)}")
输出如下:
partialFunc1.isDefinedAt(2) = true
partialFunc1.isDefinedAt(5) = true
partialFunc1.isDefinedAt(1) = false
partialFunc1.isDefinedAt(0) = false
partialFunc1(2) = Something
partialFunc1(0) = can't apply PartialFunctions where they are not defined
partialFunc2.isDefinedAt(1) = true
partialFunc2.isDefinedAt(0) = false
partialFunc2(1) = Something else
partialFunc2(0) = can't apply PartialFunctions where they are not defined
partialFunc3.isDefinedAt(0) = false
partialFunc3.isDefinedAt(1) = true
partialFunc3.isDefinedAt(2) = true
partialFunc3.isDefinedAt(3) = false
partialFunc3(1) = Something else
partialFunc3(2) = Something
类型安全的连接
Chisel可以检查不同类型的连接,下面这种连接就会报错:
- 从
Bool
/UInt
到Clock
对于其他类型,Chisel会允许我们连接,但是可能会酌情截断或者扩展数据,比如:
- 从
Bool
/UInt
到Bool
/UInt
- 从
Bundle
到Bundle
看例子:
class Bundle1 extends Bundle {
val a = UInt(8.W)
}
class Bundle2 extends Bundle1 {
val b = UInt(16.W)
}
class BadTypeModule extends Module {
val io = IO(new Bundle {
val c = Input(Clock())
val in = Input(UInt(2.W))
val out = Output(Bool())
val bundleIn = Input(new Bundle2)
val bundleOut = Output(new Bundle1)
})
// 报错,failed @: Sink (Bool) and Source (Clock) have different types.
// io.out := io.c
// 可以,但是Chisel会把io.in截断成1位来匹配io.out
io.out := io.in
// 可以,但是Chisel只会把他俩的共有部分连接起来
io.bundleOut := io.bundleIn
}
println(getVerilogString(new BadTypeModule))
输出如下:
module BadTypeModule(
input clock,
input reset,
input io_c,
input [1:0] io_in,
output io_out,
input [7:0] io_bundleIn_a,
input [15:0] io_bundleIn_b,
output [7:0] io_bundleOut_a
);
assign io_out = io_in[0]; // @[MyModule.scala 31:10]
assign io_bundleOut_a = io_bundleIn_a; // @[MyModule.scala 34:16]
endmodule
泛型(Type Generics)
Scala中的泛型
Scala的泛型(也叫做polymorphism,多型,多态)很复杂,尤其是和继承耦合到一起的时候。这一小节就简单感受一下,更多内容也还是自己去找文档看。
类的类型可以是多态的,一个很好的例子就是序列,需要知道它们元素的类型:
val seq1 = Seq("1", "2", "3") // 类型为Seq[String]
val seq2 = Seq(1, 2, 3) // 类型为Seq[Int]
val seq3 = Seq(1, "2", true) // 类型为Seq[Any]
有时候Scala编译器需要确定一个多态类型,这就需要我们显式指定类型了:
//val default = Seq() // 官方说这里会报错,但实际上测试并没有报错
val default = Seq[String]() // 用户必须告诉编译器这个序列的具体类型
println(Seq(1, "2", "3", true).foldLeft(default){
(strings, next) =>
next match {
case s: String => strings ++ Seq(s)
case _ => strings
}
})
输出为:
List(2, 3)
函数的输入和输出类型也可以是多态的。下面的例子定义了一个函数,它会对一个代码块运行消耗的时间进行测试。它基于代码块的返回值类型来参数化。注意,=> T
语法编码了一个匿名的函数,这个匿名函数没有参数列表。
def time[T](block: => T): T = {
val t0 = System.nanoTime()
val result = block
val t1 = System.nanoTime()
val timeMillis = (t1 - t0) / 1000000.0
println(s"Block took $timeMillis milliseconds!")
result
}
// 从1加到一百万
val int = time {
(1 to 1000000).reduce(_ + _) }
println(s"Add 1 through a million is $int")
// 从1到一百万,找到其中对应的16进制字符串包含beef的最大的数字
val string = time {
(1 to 1000000).map(_.toHexString).filter(_.contains("beef")).last
}
println(s"The largest number under a million that has beef: $string")
输出如下:
Block took 23.394011 milliseconds!
Add 1 through a million is 1784293664
Block took 98.548134 milliseconds!
The largest number under a million that has beef: ebeef
<