Scala介绍
什么是Scala
Scala是一种支持通用编程范式的编程语言,选择Scala作为硬件开发语言的原因如下:
- 它是托管嵌入式DSL的一种很好的的语言;
- 它具有强大而优雅的库,用于处理各种数据集合;
- 有严格的类型系统,有助于在开发周期的早期(即,编译时)捕获一大类错误;
- 具有强大的表达和传递功能功能的方式;
- Chisel比
Chipel
、Chijel
和Chicel
更顺口。(这里是一个梗,Chisel全程为Constructing Hardware in a Scala Embedded Language,这里就是把Scala替换成了其他语言,如Python,Java,C等,缩写就也跟着变了)
在讨论Chisel的时候这些点都会变得很显然,但首先要了解Scala代码的基本读写。
变量和常量——var
和val
创建变量的语句用var
(variable)关键词作为开始,常量创建用val
(value)关键词。
变量是可变的,常量是不可变的,尽可能使用常量,减少重用变量带来的错误和难以阅读。
例子:
var numberOfKittens = 6
val kittensPerHouse = 101
val alphabet = "abcdefghijklmnopqrstuvwxyz"
var done = false
注意:
- 不需要在语句后添加分号;
- Scala会在换行时推断分号(单条语句分布在多行时也是);
- 一行放多条语句时才需要分号;
var
可以被重新赋值,但是val
创建后就不可变了,例如:
numberOfKittens += 1
// kittensPerHouse = kittensPerHouse * 2 // 这句无法编译
println(alphabet)
done = true
条件语句
Scala条件语句的实现和其他语言类似:
// 一个简单的条件语句
if (numberOfKittens > kittensPerHouse) {
println("Too many kittens!!!")
}
// 在所有的分支都只有一条语句的时候就可以省略大括号
// 但Scala Style Guide建议只有存在else语句的时候才省略大括号
// (虽然能编译但是不建议)
if (numberOfKittens > kittensPerHouse)
println("Too many kittens!!!")
// 这里就可以省略
if (done)
println("we are done")
else
numberOfKittens += 1
// 存在分支有多行语句,因此不能省略大括号
if (done) {
println("we are done")
}
else if (numberOfKittens < kittensPerHouse) {
println("more kittens!")
numberOfKittens += 1
}
else {
done = true
}
但是要注意,Scala里面的if
语句会返回一个值,这个值由被选择的分支的最后一条语句决定,这个在用于初始化函数和类内的值时很有用。比如:
val likelyCharactersSet = if (alphabet.length == 26)
"english"
else
"not english"
println(likelyCharactersSet)
这里就创建了一个常量likelyCharactersSet
,但是它的值在运行时根据条件给定。
方法(函数)
方法通过def
关键词定义,在官方文档里面也abuse这个记号为函数。
函数的参数由一个通过逗号分隔的列表指定,包括参数名,参数类型,可选的是参数默认值。
需要注意的是,返回值的类型需要给定。
没有参数的Scala函数不需要空的括号,这样类成员变成函数的情况下写代码就会方便很多,因为有一些计算通过引用它来关联。按照惯例,没有参数的无副作用函数(除了返回值不会做出任何改变)不使用括号,有副作用的函数(可能会更改类变量或打印内容)应该有括号。
简单声明
// 一个简单的缩放函数,把输入乘以2,如times2(3)会返回6
// 只有一行的函数可以省略大括号
def times2(x: Int): Int = 2 * x
// 一个更复杂的函数
def distance(x: Int, y: Int, returnPositive: Boolean): Int = {
val xy = x * y
if (returnPositive) xy.abs else -xy.abs
}
函数重载
同一个函数名可以多次使用。
函数的参数列表和类型决定了函数的签名,让编译器判断应该调用哪个函数。
// 重载的函数
def times2(x: Int): Int = 2 * x
def times2(x: String): Int = 2 * x.toInt
times2(5)
times2("7")
递归和嵌套函数
大括号定义了代码的作用域。
一个函数的作用域内可能还有其他函数或递归的函数调用。在特定作用域内定义的函数仅在该作用域内可用。
// 打印倒三角形的x阵列
def asciiTriangle(rows: Int) {
// 字符串的乘法可以将字符串复制多次
def printRow(columns: Int): Unit = println("X" * columns)
if(rows > 0) {
printRow(rows)
asciiTriangle(rows - 1) // 这里是递归调用
}
}
// printRow(1) // 该函数调用不在作用域内,编译不通过
asciiTriangle(6)
列表
Scala实现了各种聚合的或序列的对象。
列表和数组类似,但是支持额外的附加(appending)和提取(extracting)操作。
val x = 7
val y = 14
val list1 = List(1, 2, 3)
val list2 = x :: y :: y :: Nil // 列表的另一种表示法
val list3 = list1 ++ list2 // 把第二个列表附加到第一个列表
val m = list2.length
val s = list2.size
val headOfList = list1.head // 获取列表的第一个元素
val restOfList = list1.tail // 获取移除了列表中第一个元素的列表
val third = list1(2) // 获取列表的第三个元素,从0开始索引
for
语句
Scala有for
语句,和传统的for语句类似,可以在一个范围上迭代值。
for (i <- 0 to 7) { print(i + "") }
println()
如果用until
替换to
,那就会从0迭代到6,即不会包括7。
for (i <- 0 until 7) { print(i + "") }
println()
by
可以指定固定的增量,比如这样可以输出0-10之间的所有整数:
for(i <- 0 to 10 by 2) { print(i + " ") }
println()
如果有个集合想访问它的所有元素,可以使用for
作为迭代器,和Java以及Python里面是一样的。
这里就创建了一个四随机数元素的列表,然后相加:
// 这个随机数生成不太优雅的样子
val randomList = List(scala.util.Random.nextInt(), scala.util.Random.nextInt(), scala.util.Random.nextInt(), scala.util.Random.nextInt())
var listSum = 0
for (value <- randomList) {
listSum += value
}
println("sum is " + listSum)
for
很好用,但不是最方便的。
比如对数组元素求和,通过叫做comprehensions
的函数族来计算更方便。
后边的部分也会讲更多关于for
和它的同类。
阅读Scala代码
要称为高效的Chisel设计师,应该:
- 能够读懂Scala代码;
- 理解常见的命名惯例;
- 理解常见的设计模式;
- 理解常见的最佳实践;
Chisel的魅力之一是代码重用,如果看不懂别人的代码就很难重用。
有效解析别人的代码也更容易寻求帮助,比如从网上搜索时知道怎么搜,怎么在论坛上提问。
下面首先讲讲常见的代码模式。
包和导入
package mytools
class Tool1 { ... }
当需要引用定义了以上代码的一个文件时,应该这么写:
import mytools.Tool1
注意:包的名字需要匹配路径层级。这不是强制性的,但不遵守的话可能会产生一些难以定位的bug。
按照惯例,包名称是小写的,并且不包含下划线之类的分隔符。这样就不好起一个有好的描述性的包名,方法就是添加层级,比如package good.tools
。尽量吧,Chisel本身也会搞些不遵守这个规范的事情。
以上,import
语句告知编译器你要使用一些额外的库,Chisel编程中常用的导入如下:
import chisel3._
import chisel3.iotesters.{ChiselFlatSpec, Driver, PeekPokeTester}
第一句会把chisel3
里面所有的类和方法导入,_
表示通配符。
第二句从chisel3.iotesters
中导入了指定的类。
Scala是一种面向对象的语言
Scala是面向对象的,所以稍微理解一些可以有利于最大化Scala和Chisel的优势。
关于面向对象的描述有很多,这里官方文档给了一些:
- 变量是对象;
- 通过
val
定义的常量是对象; - 字面值(固定值)本身是对象;
- 函数也是对象;
- 对象是类的实例;
- 事实上,在Scala中几乎所有重要的东西,面向对象中的对象都被称为实例;
- 在定义类时,程序员指定;
- 数据(
val
和var
)和类相关联; - 类的实例可以执行的操作,称为方法或函数;
- 类可以扩展为其他类;
- 被扩展的类是超类,扩展对象是子类;
- 子类从超类继承数据和方法;
- 有一些方法可以让类扩展或覆盖继承的属性;
- 类可以从特征(traits)继承,Traits可以理解为轻量级的类,允许从多个超类继承特定的、有限的方式;
- 单例(Singleton)对象只一种特殊的Scala类;
- 它们不是上述对象,我们把它们叫作实例。
现在来看看在Scala中如何定义一个类。
一个类的例子
在Scala中创建一个类可以是这样的:
// WrapCounter计数到根据位大小确定的最大值
class WrapCounter(counterBits: Int) {
val max: Long = (1 << counterBits) - 1
var counter = 0L
def inc(): Long = {
counter = counter + 1
if (counter > max) {
counter = 0
}
counter
}
println(s"counter created with max value $max")
}
包括:
-
class WrapCounter
:WrapCounter
的定义; -
(counterBits: Int)
:创建该对象需要一个整数参数,通过命名可以提示参数含义; - 大括号划定了代码块,大多数类用一个代码块来定义变量、常量和方法(函数);
-
val max: Long =
:这个类包含一个成员变量max
,声明为Long
类型,当类创建的时候被初始化; -
(1 << counterBits) - 1
计算counterBits
位可以存放的最大值,因为max
是创建为val
类型的所以不会改变; - 变量
counter
被创建并初始化为0L
,L
表示0
是一个Long
类型的值,因此counter
被推断为Long
类型; -
max
和counter
都被称为类的成员变量; - 类方法
inc
定义为不接受任何参数并返回Long
值的方法; -
inc
方法的函数体包括:-
counter = counter + 1
执行counter
的自增1操作; -
if (counter > max) { counter = 0 }
测试counter
是否大于max
的值,如果成立则将counter
置为0; -
counter
:这最后一行很重要,代码块的最后一行的表达式的值被认为是代码块的返回值,这个返回值可以使用也可以忽略,这个用法很常见,比如val result = if (10 * 10 > 90) "greater" else "lesser"
就会创建一个val
,其值为"greater"
- 所以在这个例子中,函数
inc
会返回counter
的值;
-
-
println(s"counter created with max value $max")
:打印字符串到标准输出。由于println
是直接在代码块中定义的,是类初始化代码的一部分,会被执行,即输出字符串,每次这个类的实例创建值都会执行; - 这个例子中被打印的字符串是一个插值(interpolated)字符串:
- 双引号前面开头的
s
表示这个一个插值字符串; - 插值字符串会在运行时处理;
-
$max
会被max
的值取代; - 如果
$
后面跟着的是代码块,任意的Scala语句可以包含在代码块中:- 比如
println(s"doubled max is ${max + max}")
; - 代码块的返回值会被插入来替换
${...}
; - 如果返回值不是个字符串,那就会被转换为字符串,scala中几乎每个类或类型都有定义了的到字符串的转换;
- 比如
- 一般需要避免在每次创建实例时都打印东西防止标注输出一大堆,除非是在调试;
- 双引号前面开头的
创建一个类的实例
Scala实例通过内置关键词new
来创建:
val x = new WrapCounter(2)
也有很多不使用new
关键词的情况,比如val y = WrapCounter(6)
,这种情况需要特别注意,但需要伴生对象的使用,后面会详细提到。
实例的使用例子如下:
x.inc() // counter自增
// 实例x的成员变量对外是可见的,除非被声明为private
if(x.counter == x.max) {
println("counter is about to wrap")
}
x inc() // Scala允许不使用点,这有助于让嵌入式DSL看起来更自然
代码块
代码块由大括号划定,一个代码块可以包含0行或多行代码,最后一行会返回值。
没有代码的代码块会返回一个类似null
的对象,叫做Unit
。
Scala中遍布代码块,比如类定义的主体,函数方法的定义,if
语句的定义,for
的主体等。
参数化代码块
代码块可以接收参数。
在类和方法的定义中,这些参数看起来和其他传统编程语言一样。
下面的例子中,c
和s
是代码块的参数:
// 只有一行的代码块不需要大括号
def add1(c: Int): Int = c + 1
class RepeatString(s: String) {
val repeatedString = s + s
}
注意!还有其他方法可以参数化代码块,比如:
val intList = List(1, 2, 3)
val stringList = intList.map { i =>
i.toString
}
代码块被传递给List
类中的方法map
,这个方法需要它的代码块有单个参数。为列表的每个成员调用代码块,代码块返回转换为字符串的成员。这种写法就是匿名函数,在Scala中有各种变体可用。后续会更详细介绍。
这里是为了帮助在遇到各种符号时认识他们。这里是官方文档倾向于的风格,特定情况下其他风格可能会更自然。单行代码风格倾向于更简洁的形式,复杂块通常具有叙事性的表现。
要想更容易协作的话,推荐看Scala Style Guide。
命名参数和默认参数值
看看下面的方法定义:
def myMethod(count: Int, wrap: Boolean, wrapValue: Int = 24): Unit = { ... }
调用这个方法的时候,通常会看到给出了传入值对应的参数名:
myMethod(count = 10, wrap = false, wrapValue = 23)
使用命名参数,甚至可以以不同的参数顺序调用函数:
myMethod(wrapValue = 23, wrap = false, count = 10)
对于经常调用的方法,参数顺序可能是显而易见的,但是对于不太常见的方法,特别是布尔型参数,包含命名参数可以使得代码更有可读性。如果一个方法有很多相同类型的参数,使用命名参数也可以减少错用的情况。
类的定义也可以使用这种命名参数的构造方法。
当特定的参数有默认值(不需要被覆盖)的时候,调用者只需要(按名称)传递没有默认值的参数。比如wrapValue
的默认值为24,因此:
myMethod(wrap = false, count = 10)
会按照24被传入了一样调用函数。