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 = 6val kittensPerHouse = 101val 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 * xdef times2(x: String): Int = 2 * x.toInttimes2(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 = 7val y = 14val list1 = List(1, 2, 3)val list2 = x :: y :: y :: Nil // 列表的另一种表示法val list3 = list1 ++ list2 // 把第二个列表附加到第一个列表val m = list2.lengthval s = list2.sizeval 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 = 0for (value <- randomList) { listSum += value}println("sum is " + listSum)
for很好用,但不是最方便的。
比如对数组元素求和,通过叫做comprehensions的函数族来计算更方便。
后边的部分也会讲更多关于for和它的同类。
阅读Scala代码要称为高效的Chisel设计师,应该:
能够读懂Scala代码;理解常见的命名惯例;理解常见的设计模式;理解常见的最佳实践;Chisel的魅力之一是代码重用,如果看不懂别人的代码就很难重用。
有效解析别人的代码也更容易寻求帮助,比如从网上搜索时知道怎么搜,怎么在论坛上提问。
下面首先讲讲常见的代码模式。
包和导入package mytoolsclass 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的成员变量对外是可见的,除非被声明为privateif(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 + 1class 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被传入了一样调用函数。