Functional Programming in Scala

Scala functions were introduced in Module 1, and you saw then used a lot in the previous module. Here's a refresher on functions. Functions take any number of inputs and produce one output. Inputs are often called arguments to a function. To produce no output, return the Unit type.

Example: Custom Functions
Below are some examples of functions in Scala.

// no inputs or outputs (two versions)

def hello1(): Unit = print("Hello!")

def hello2 = print("Hello again!")

​

// math operation, one input and one output

def times2(x: Int): Int = 2 * x

​

// inputs can have default values, and the return type is optional

def timesN(x: Int, n: Int = 2) = n * x

​

// call the functions listed above

hello1()

hello2

times2(4)

timesN(4) // no need to specify n to use the default value

timesN(4, 3) // argument order is the same as the order where the function was defined

timesN(n=7, x=2) // arguments may be reordered and assigned to explicitly

Functions as Objects

Functions in Scala are first-class objects. That means we can assign a function to a val and pass it to classes, objects, or other functions as an argument.

Example: Function Objects
Below are the same functions implemented as functions and as objects.

// these are normal functions

def plus1funct(x: Int): Int = x + 1

def times2funct(x: Int): Int = x * 2

​

// these are functions as vals

// the first one explicitly specifies the return type

val plus1val: Int => Int = x => x + 1

val times2val = (x: Int) => x * 2

​

// calling both looks the same

plus1funct(4)

plus1val(4)

plus1funct(x=4)

//plus1val(x=4) // this doesn't work

Why would you want to create a val instead of a def? With a val, you can now pass the function around to other functions, as shown below. You can even create your own functions that accept other functions as arguments. Formally, functions that take or produce functions are called higher-order functions. You saw them used in the last module, but now you'll make your own!

Example: Higher-Order Functions
Here we show map again, and we also create a new function, opN, that accepts a function, op, as an argument.

// create our function

val plus1 = (x: Int) => x + 1

val times2 = (x: Int) => x * 2

​

// pass it to map, a list function

val myList = List(1, 2, 5, 9)

val myListPlus = myList.map(plus1)

val myListTimes = myList.map(times2)

​

// create a custom function, which performs an operation on X N times using recursion

def opN(x: Int, n: Int, op: Int => Int): Int = {

if (n <= 0) { x }

else { opN(op(x), n-1, op) }

}

​

opN(7, 3, plus1)

opN(7, 3, times2)

Example: Functions vs. Objects
A possibly confusing situation arises when using functions without arguments. Functions are evaluated every time they are called, while vals are evaluated at instantiation.

import scala.util.Random

​

// both x and y call the nextInt function, but x is evaluated immediately and y is a function

val x = Random.nextInt

def y = Random.nextInt

​

// x was previously evaluated, so it is a constant

println(s"x = $x")

println(s"x = $x")

​

// y is a function and gets reevaluated at each call, thus these produce different results

println(s"y = $y")

println(s"y = $y")

Anonymous Functions

As the name implies, anonymous functions are nameless. There's no need to create a val for a function if we'll only use it once.

Example: Anonymous Functions
The following example demonstrates this. They are often scoped (put in curly braces instead of parentheses).

val myList = List(5, 6, 7, 8)

​

// add one to every item in the list using an anonymous function

// arguments get passed to the underscore variable

// these all do the same thing

myList.map( (x:Int) => x + 1 )

myList.map(_ + 1)

​

// a common situation is to use case statements within an anonymous function

val myAnyList = List(1, 2, "3", 4L, myList)

myAnyList.map {

case (:Int|:Long) => "Number"

case _:String => "String"

case _ => "error"

}

Exercise: Sequence Manipulation
A common set of higher-order functions you'll use are scanLeft/scanRight, reduceLeft/reduceRight, and foldLeft/foldRight. It's important to understand how each one works and when to use them. The default directions for scan, reduce, and fold are left, though this is not guaranteed for all cases.

val exList = List(1, 5, 7, 100)

​

// write a custom function to add two numbers, then use reduce to find the sum of all values in exList

def add(a: Int, b: Int): Int = ???

val sum = ???

​

// find the sum of exList using an anonymous function (hint: you've seen this before!)

val anon_sum = ???

​

// find the moving average of exList from right to left using scan; make the result (ma2) a list of doubles

Let's look at some examples of how to use functional programming when creating hardware generators in Chisel.

Example: FIR Filter
First, we'll revisit the FIR filter from previous examples. Instead of passing in the coefficients as parameters to the class or making them programmable, we'll pass a function to the FIR that defines how the window coefficients are calculated. This function will take the window length and bitwidth to produce a scaled list of coefficients. Here are two example windows. To avoid fractions, we'll scale the coefficients to be between the max and min integer values. For more on these windows, check out the this Wikipedia page.

// check it out! first argument is the window length, and second argument is the bitwidth

TriangularWindow(10, 16)

HammingWindow(10, 16)

Now we'll create a FIR filter that accepts a window function as the argument. This allows us to define new windows later on and retain the same FIR generator. It also allows us to independently size the FIR, knowing the window will be recalculated for different lengths or bitwidths. Since we are choosing the window at compile time, these coefficients are fixed.

Those last three lines could be easily combined into one. Also notice how we've handled bitwidth growth conservatively to avoid loss.

Example: FIR Filter Tester
Let's test our FIR! Previously, we provided a custom golden model. This time we'll use Breeze, a Scala library of useful linear algebra and signal processing functions, as a golden model for our FIR filter. The code below compares the Chisel output with the golden model output, and any errors cause the tester to fail.

Try uncommenting the print statment at the end just after the expect call. Also try changing the window from triangular to Hamming.

Complete the following exercises to practice writing functions, using them as arguments to hardware generators, and avoiding mutable data.

Exercise: Neural Network Neuron
Our first example will have you build a neuron, the building block of fully-connected layers in artificial neural networks. Neurons take inputs and a set of weights, one per input, and produce one output. The weights and inputs are multiplied and added, and the result is fed through an activation function. In this exercise, you will implement different activation functions and pass them as an argument to your neuron generator.

Neuron

First, complete the following code to create a neuron generator. We'll make the inputs and outputs 16-bit fixed point values with 8 fractional bits.

Finally, let's create a tester that checks the correctness of our Neuron. With the step activation function, neurons may be used as logic gate approximators. Proper selection of weights can perform AND, OR, NOT, and other binary functions. Since NOT requires a bias (which we did not implement) and XOR requires chaining multiple neurons, we'll test our neuron just with AND and OR logic. Complete the following tester to check our neuron with the step function.

Note that since the circuit is purely combinational, the reset(5) and step(1) calls are not necessary.