Chisel logo

Module 1: Introduction to Scala

Next: Your First Chisel Module

Motivation

These tutorials use the Jupyter notebook environment so you can read code, make changes, and then run code snippets in place. We encourage you to experiment with tutorial code blocks to speed your way to Chisel mastery.

In this first module, you will learn how to write basic Scala code and how to read more advanced Scala code.


Understanding Scala

Scala is yet another programming language which supports common programming paradigms. We chose to use it for several reasons:

  • It is a good language for hosting an embedded DSL.
  • It has a powerful and elegant library for manipulating various collections of data.
  • It has a rigorous type system that helps catch a large class of errors very early in the development cycle, i.e. at compilation.
  • It has powerful ways of expressing and passing functions.
  • Chipel, Chijel, and Chicel don't roll off the tongue as nicely as Chisel does.

All of these points will become apparent as we talk about Chisel later, but for now, we are going to focus on the basics of reading and writing Scala code.


Variables and Constants - var and val

Statements that create (mutable) variables and constant values (immutable variables) are preceded with the keywords var and val respectively. It is common practice to use val whenever possible. Why? Mostly to reduce the chances of re-using variables in ways that are error prone or make your code difficult to read. The structure of Scala makes this practice easier than you might expect.

**Example:**
The subsequent code block is executable right here, within this notebook. To run it, focus on it by clicking it. When the cell is active, a box with a green bar on the left appears around it. Once selected, the active code block cell may be run using the play button found at the top of the notebook (highlighted in red below for reference).

Alternatively, you may use keyboard shortcuts. Shift+enter runs the currently active cell and moves the focus to the next cell. Ctrl+enter runs the current cell and keeps it in focus.

Key examples begin with a blue and bold **Example**, while exercises begin with a red **Exercise**. Module 1 consists entirely of short examples, so we've omitted the example declaration for the rest of this module.

In [ ]:
var numberOfKittens = 6
val kittensPerHouse = 101
val alphabet = "abcdefghijklmnopqrstuvwxyz"
var done = false

One of the first things to notice is that unlike Java and C, Scala does not generally require semicolons at the end of statements. Scala infers the semicolon when there is a line feed. For instance, it can usually tell if a single statement is spread across multiple lines when the last thing on the line is an operater requiring additional code. The only time you need a semicolon is when you want to fit multiple statements onto one line.

You use variables in obvious ways. The two vars may be reassigned to, while the two vals are immutable once created.

In [ ]:
numberOfKittens += 1

// kittensPerHouse = kittensPerHouse * 2 // This would not compile; kittensPerHouse is not updatable

println(alphabet)
done = true

Conditionals

Scala implements conditionals like other programming languages.

In [ ]:
// A simple conditional; by the way, this is a comment
if (numberOfKittens > kittensPerHouse) { 
    println("Too many kittens!!!") 
}
// The braces are not required when all branches are one liners. However, the 
// Scala Style Guide prefers brace omission only if an "else" clause is included.
// (Preferably not this, even though it compiles...)
if (numberOfKittens > kittensPerHouse) 
    println("Too many kittens!!!")

// ifs have else clauses, of course
// This is where you can omit braces!
if (done) 
    println("we are done")
else 
    numberOfKittens += 1

// And else ifs
// For style, keep braces because not all branches are one liners. 
if (done) {
    println("we are done")
}
else if (numberOfKittens < kittensPerHouse) {
    println("more kittens!")
    numberOfKittens += 1
}
else {
    done = true
}

But in Scala, an "if" conditional returns a value. What is that value? It's given by the last line of the selected branch. It's quite powerful, particularly when used to initialize values in functions and classes. It looks like:

In [ ]:
val likelyCharactersSet = if (alphabet.length == 26)
    "english"
else 
    "not english"

println(likelyCharactersSet)

We created a constant likelyCharactersSet whose value is conditionally determined at runtime.


Methods (Functions)

Methods are defined with the keyword def. Here, we'll abuse notation and also refer to them as functions. Function arguments (or parameters) are specified in a comma separated list that specifies the name of the argument, its type, and optionally a default value for it. Return types should be specified for clarity.

Scala functions that do not have any arguments do not require empty parentheses. This often makes life easier for a developer in the situation where a member of a class becomes a function because there is some computation associated with referencing it. By convention, argument-less functions that do not have side effects (i.e. calling them does not change anything and they simply return a value) do not use parentheses, and functions that do have side effects (perhaps they change class variables or print stuff out) should require parentheses.

Simple Declarations

In [ ]:
// Simple scaling function with an input argument, e.g., times2(3) returns 6
// Curly braces can be omitted for short one-line functions.
def times2(x: Int): Int = 2 * x

// More complicated function
def distance(x: Int, y: Int, returnPositive: Boolean): Int = {
    val xy = x * y
    if (returnPositive) xy.abs else -xy.abs
}

Overloading Functions

The same function name can be used in more than one way. The parameters and their types determine a signature that allows the compiler to figure out which version of the function should be called. Overloaded functions should be avoided, though.

In [ ]:
// Overloaded function
def times2(x: Int): Int = 2 * x
def times2(x: String): Int = 2 * x.toInt

times2(5)
times2("7")

Recursive and Nested Functions

Curly braces define code scopes. Within a function's scope may exist more functions or recursive function calls. Functions defined in a certain scope are only accessible within that scope.

In [ ]:
/** Prints a triangle made of "X"s
  * This is another style of comment
  */
def asciiTriangle(rows: Int) {
    
    // This is cute: multiplying "X" makes a string with many copies of "X"
    def printRow(columns: Int): Unit = println("X" * columns)
    
    if(rows > 0) {
        printRow(rows)
        asciiTriangle(rows - 1) // Here is the recursive call
    }
}

// printRow(1) // This would not work, since we're calling printRow outside its scope
asciiTriangle(6)

Lists

Scala implements a variety of aggregate or sequence objects. Lists are a lot like arrays but support additional operations for appending and extracting.

In [ ]:
val x = 7
val y = 14
val list1 = List(1, 2, 3)
val list2 = x :: y :: y :: Nil       // An alternate notation for assembling a list

val list3 = list1 ++ list2           // Appends the second list to the first list
val m = list2.length
val s = list2.size

val headOfList = list1.head          // Gets the first element of the list
val restOfList = list1.tail          // Get a new list with first element removed

val third = list1(2)                 // Gets the third element of a list (0-indexed)

for Statement

Scala has a for statement and it works like traditional for statements. You can iterate over a range of values.

In [ ]:
for (i <- 0 to 7) { print(i + " ") }
println()

Use until instead of to for iterating from 0 to 6 (7 is not included).

In [ ]:
for (i <- 0 until 7) { print(i + " ") }
println()

Add a by to increment by some fixed amount. The following prints out the even integers between 0 and 10.

In [ ]:
for(i <- 0 to 10 by 2) { print(i + " ") }
println()

If you have a collection of some kind and want to visit all of the elements, you can use for as an iterator, as in Java and Python. Here we make a list of 4 random integers and then sum them.

In [ ]:
val randomList = List(util.Random.nextInt(), util.Random.nextInt(), util.Random.nextInt(), util.Random.nextInt())
var listSum = 0
for (value <- randomList) {
  listSum += value
}
println("sum is " + listSum)

Scala's for has a lot more tricks it can do. It will work intuitively for a wide range of traditional iteration needs, but it may or may not be the most convenient thing to use. Operations like summing the elements of an array are often more easily done using a rich family of functions called comprehensions that are available across many different collections of elements. Later modules will provide more on for and its allies.


Reading Scala

Being able to read Scala code and understand common naming conventions, design patterns, and best practices is an important step in becoming an effective Chisel designer. The potential for code reuse is one of the advantages of Chisel, but reuse is difficult if you can't read other people's code. Effectively parsing other people's code also makes it easier to seek out help, especially from resources like StackOverflow.

The following sections show common code patterns you will see.


Packages and Imports

package mytools
class Tool1 { ... }

When externally referencing code defined in a file containing the above lines, one should use:

import mytools.Tool1

Note: The package name should match the directory hierarchy. This is not mandatory, but failing to abide by this guideline can produce some unusual and difficult to diagnose problems. Package names by convention are lower case and do not contain separators like underscores. This sometimes makes good descriptive names difficult. One approach is to add a layer of hierarchy, e.g. package good.tools. Do your best. Chisel itself plays some games with the package names that do not conform to these rules.

As shown above, import statements inform the compiler that you are using some additional libraries. Some common imports you will use when programming in Chisel are:

import chisel3._
import chisel3.iotesters.{ChiselFlatSpec, Driver, PeekPokeTester}

The first imports all the classes and methods in the chisel3 package; the underscore here works as a wildcard. The second imports specific classes from the chisel3.iotesters package.


Scala Is an Object Oriented Language

Scala is object oriented, and it's important to understand a bit about this to take maxiumum advantage of both Scala and Chisel. There is no doubt more than one way to describe all this.

  1. Variables are objects.
  2. Constants in the sense of Scala's val declarative are also objects.
  3. Even literal values are objects.
  4. Even functions themselves are objects. More on this later.
  5. Objects are instances of classes.
    1. In fact, in just about every way that matters in Scala, the object in Objected Oriented will be called an instance.
  6. In defining classes, the programmer specifies:
    1. The data (val, var) associated with the class.
    2. The operations, called methods or functions, that instances of the class can perform.
  7. Classes can extend other classes.
    1. The class being extended is the superclass; the extendee is the subclass.
    2. In this case, the subclass inherits the data and methods from the superclass.
    3. There are many useful but controlled ways in which a class may extend or override inherited properties.
  8. Classes may inherit from traits. Think of traits as lightweight classes that allow specific, limited ways of inheriting from more than one superclass.
  9. (Singleton) Objects are a special kind of Scala class.
    1. They are not objects as above. Remember, we're calling those instances.

We are about to look at how to create a class in Scala.


A Class Example

An example of creating a Scala class might be

In [ ]:
// WrapCounter counts up to a max value based on a bit size
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")
}

What is here:

  • class WrapCounter -- This is the definition of WrapCounter.
  • (counterBits: Int) -- Creating a WrapCounter requires an integer parameter, nicely named to suggest it is the bit width of the counter.
  • Braces ({}) delimit a block of code. Most classes use a code block to define variables, constants, and methods (functions).
  • val max: Long = -- the class contains a member variable max, declared as type Long and initialized as the class is created.
  • (1 << counterBits) - 1 computes the maximum value that can be contained in counterBits bits. Since max was created with val it cannot be changed.
  • A variable counter is created and initialized to 0L. The L says that 0 is a long value; thus, counter is inferred to be Long.
  • max and counter are commonly called member variables of the class.
  • A class method inc is defined which takes no arguments and returns a Long value.
  • The body of the method inc is a code block that has:
    • counter = counter + 1 increments counter.
    • if (counter > max) { counter = 0 } tests if the counter is greater than the max value and sets it back to zero if it is.
    • counter -- The last line of the code block is important.
      • Any value expressed as the last line of a code block is considered to be the return value of that code block. The return value can be used or ignored by the calling statement.
      • This applies quite generally; for example, since an if then else statement defines its true and false clauses with code blocks, it can return a value i.e., val result = if (10 * 10 > 90) "greater" else "lesser" would create a val with the value "greater".
    • So in this case the function inc returns the value of counter.
  • println(s"counter created with max value $max") prints a string to standard output. Because the println is directly in the defining code block, it is part of the class initialization code and is run, i.e. prints out the string, every time an instance of this class is created.
  • The string printed in this case is an interpolated string.
    • The leading s in front of the first double quote identifies this as an interpolated string.
    • An interpolated string is processed at run time.
    • The \$max is replaced with the value of max.
    • If the \$ is followed by a code block, arbitrary Scala can be in that code block.
      • For example, println(s"doubled max is ${max + max}").
      • The return value of this code block will be inserted in place of ${...}.
      • If the return value is not a string, it will be converted to one; virtually every class or type in scala has an implicit conversion to a string defined).
    • You should generally avoid printing something every time an instance of a class is created to avoid flooding standard output, unless you're debugging.

Creating an Instance of a Class

Let's use our example above to create a class. Scala instances are created via the built-in magic keyword new.

In [ ]:
val x = new WrapCounter(2)

You may see instances being created without the keyword new i.e., val y = WrapCounter(6). This occurs often enough to merit special attention, but requires the use of a companion object. It is described later.

Example usage of the instance that has just been created is given next. (Try evaluating the cell below twice.)

In [ ]:
x.inc() // Increments the counter

// Member variables of the instance x are visible to the outside, unless they are declared private
if(x.counter == x.max) {              
    println("counter is about to wrap")
}

x inc() // Scala allows the dots to be omitted; this can be useful for making embedded DSL's look more natural

Code Blocks

Code blocks are delimited by braces. A block can contain zero or more lines of Scala code. The last line of Scala code becomes the return value (which may be ignored) of the code block. A code block with no lines would return a special null-like object called Unit. Code blocks are used throughout Scala: they are the bodies of class definitions, they form function and method definitions, they are the clauses of if statements, and they are the bodies of for and many other Scala operators.

Parameterized Code Blocks

Code blocks can take parameters. In the case of class and method definitions, these parameters look like those in most conventional programming languages. In the example below, c and s are parameters of the code blocks.

In [ ]:
// A one-line code block doesn't need to be enclosed in {}
def add1(c: Int): Int = c + 1

class RepeatString(s: String) {
  val repeatedString = s + s
}

IMPORTANT: There is another way in which code blocks may be parameterized. Here is an example.

In [ ]:
val intList = List(1, 2, 3)
val stringList = intList.map { i =>
  i.toString
}

The code block is passed to a method map of the class List. The map method requires that its code block have a single parameter. The code block is called for each member of the list, and the code block returns that member converted to a String. Scala is almost excessively accepting of variations of this syntax. You might see this written in many different ways. This type of code block is called an anonymous function, and more details on anonymous functions are provided in a later module.

The goal here is to help you recognize the different notational types when you encounter them. As you use Scala, these will seem more comfortable and familiar. Authors tend to gravitate to particular styles, and there are also individual syntactical situations in which one notation will seem more natural. One-liners tend to use the more concise forms. Complex blocks usually have a more narrative appearance. To make collaboration easier, one should consider skimming through best practices found in the Scala Style Guide.


Named Parameters and Parameter Defaults

Consider the following method definition.

def myMethod(count: Int, wrap: Boolean, wrapValue: Int = 24): Unit = { ... }

When calling the method, you will often see the parameter names along with the passed-in values.

myMethod(count = 10, wrap = false, wrapValue = 23)

Using named parameters, you can even call the function with a different ordering.

myMethod(wrapValue = 23, wrap = false, count = 10)

For frequently called methods, the parameter ordering may be obvious. But for less common methods and, in particular, boolean arguments, including the names with calls can make your code a lot more readable. If methods have a long list of arguments of the same type, using names also decreases the chance of error. Parameters to class definitions also use this named argument scheme (they are actually just the parameters to the constructor method for the class).

When certain parameters have default values (that don't need to be overridden), callers only have to pass (by name) specific arguments that do not use defaults. Notice that the parameter wrapValue has a default value of 24. Therefore,

myMethod(wrap = false, count = 10)

will work as if 24 had been passed in.


You're done!

Return to the top.