Chisel教程——01.Scala介绍

Scala介绍

什么是Scala

Scala是一种支持通用编程范式的编程语言,选择Scala作为硬件开发语言的原因如下:

  1. 它是托管嵌入式DSL的一种很好的的语言;
  2. 它具有强大而优雅的库,用于处理各种数据集合;
  3. 有严格的类型系统,有助于在开发周期的早期(即,编译时)捕获一大类错误;
  4. 具有强大的表达和传递功能功能的方式;
  5. Chisel比ChipelChijelChicel更顺口。(这里是一个梗,Chisel全程为Constructing Hardware in a Scala Embedded Language,这里就是把Scala替换成了其他语言,如Python,Java,C等,缩写就也跟着变了)

在讨论Chisel的时候这些点都会变得很显然,但首先要了解Scala代码的基本读写。

变量和常量——varval

创建变量的语句用var(variable)关键词作为开始,常量创建用val(value)关键词。

变量是可变的,常量是不可变的,尽可能使用常量,减少重用变量带来的错误和难以阅读。

例子:

var numberOfKittens = 6
val kittensPerHouse = 101
val alphabet = "abcdefghijklmnopqrstuvwxyz"
var done = false

注意:

  1. 不需要在语句后添加分号;
  2. Scala会在换行时推断分号(单条语句分布在多行时也是);
  3. 一行放多条语句时才需要分号;

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设计师,应该:

  1. 能够读懂Scala代码;
  2. 理解常见的命名惯例;
  3. 理解常见的设计模式;
  4. 理解常见的最佳实践;

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的优势。

关于面向对象的描述有很多,这里官方文档给了一些:

  1. 变量是对象;
  2. 通过val定义的常量是对象;
  3. 字面值(固定值)本身是对象;
  4. 函数也是对象;
  5. 对象是类的实例;
  6. 事实上,在Scala中几乎所有重要的东西,面向对象中的对象都被称为实例;
  7. 在定义类时,程序员指定;
  8. 数据(valvar)和类相关联;
  9. 类的实例可以执行的操作,称为方法或函数;
  10. 类可以扩展为其他类;
  11. 被扩展的类是超类,扩展对象是子类;
  12. 子类从超类继承数据和方法;
  13. 有一些方法可以让类扩展或覆盖继承的属性;
  14. 类可以从特征(traits)继承,Traits可以理解为轻量级的类,允许从多个超类继承特定的、有限的方式;
  15. 单例(Singleton)对象只一种特殊的Scala类;
  16. 它们不是上述对象,我们把它们叫作实例。

现在来看看在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")
}

包括:

  1. class WrapCounterWrapCounter的定义;
  2. (counterBits: Int):创建该对象需要一个整数参数,通过命名可以提示参数含义;
  3. 大括号划定了代码块,大多数类用一个代码块来定义变量、常量和方法(函数);
  4. val max: Long =:这个类包含一个成员变量max,声明为Long类型,当类创建的时候被初始化;
  5. (1 << counterBits) - 1计算counterBits位可以存放的最大值,因为max是创建为val类型的所以不会改变;
  6. 变量counter被创建并初始化为0LL表示0是一个Long类型的值,因此counter被推断为Long类型;
  7. maxcounter都被称为类的成员变量;
  8. 类方法inc定义为不接受任何参数并返回Long值的方法;
  9. inc方法的函数体包括:
    1. counter = counter + 1执行counter的自增1操作;
    2. if (counter > max) { counter = 0 }测试counter是否大于max的值,如果成立则将counter置为0;
    3. counter:这最后一行很重要,代码块的最后一行的表达式的值被认为是代码块的返回值,这个返回值可以使用也可以忽略,这个用法很常见,比如val result = if (10 * 10 > 90) "greater" else "lesser"就会创建一个val,其值为"greater"
    4. 所以在这个例子中,函数inc会返回counter的值;
  10. println(s"counter created with max value $max"):打印字符串到标准输出。由于println是直接在代码块中定义的,是类初始化代码的一部分,会被执行,即输出字符串,每次这个类的实例创建值都会执行;
  11. 这个例子中被打印的字符串是一个插值(interpolated)字符串:
    1. 双引号前面开头的s表示这个一个插值字符串;
    2. 插值字符串会在运行时处理;
    3. $max会被max的值取代;
    4. 如果$后面跟着的是代码块,任意的Scala语句可以包含在代码块中:
      1. 比如println(s"doubled max is ${max + max}");
      2. 代码块的返回值会被插入来替换${...}
      3. 如果返回值不是个字符串,那就会被转换为字符串,scala中几乎每个类或类型都有定义了的到字符串的转换;
    5. 一般需要避免在每次创建实例时都打印东西防止标注输出一大堆,除非是在调试;

创建一个类的实例

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的主体等。

参数化代码块

代码块可以接收参数。

在类和方法的定义中,这些参数看起来和其他传统编程语言一样。

下面的例子中,cs是代码块的参数:

// 只有一行的代码块不需要大括号
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被传入了一样调用函数。

上一篇:Scala中String方法


下一篇:Centos6查看mysql-server信息