Scala Function Tricks

Functions versus Procedures

A procedure is like a function, but instead of returning a value, it causes a side-effect to happen.

Examples of side effects include:

·       Updating a variable

·       Writing to a file or stream

·       Throwing an exception

In languages like Java or C procedures are functions with a void return type. In Scala a procedure has a Unit return type.

For example, deposit and showBalance are procedures:

var balance = 99.0
def deposit(amt: Double): Unit = balance += amt
def showBalance: Unit = println("balance = $" + balance)

Technically, procedures return the only value of type Unit, written ().

() prints out as the empty string.

When we say a function returns a value, we mean that it returns an interesting non-void value.

A pure function returns a value without any side effect.

A hybrid function returns a value and produces a side effect. For example:

def cube(x: Double) =
  val result = x * x * x
  println(s"$x cubed = $result")
  result

Here's a sample call:

scala> cube(3)
3.0 cubed = 27.0
val res4: Double = 27.0

·       To be clear, the last line is Scala's repl printing the value, 27, to the console window. The line before is the side effect of the println call.

·       This might seem better than a boring function that just cubes numbers, but it makes cube unusable in applications that call cube as a low-level function.

·       Avoid hybrids!

Functions versus Methods

A method has an implicit parameter called this.

For example:

class Gladiator(val name: String) {
  var health: Int = 100
  def damage(amt: Int) = this.health = math.max(0, this.health – amt)
  def attack(opponent: Gladiator) = {
     this.damage(1) // attacking damages the attacker a little bit
     opponent.damage(5)
  }
  // etc.
}

object Test extends App {
  val tania = Gladiator("Tania")
  val sonya = Gladiator("Sonya")
  tania.attack(sonya)
  sonya.attack(tania)
  // etc.
}

The attack method has two parameters, the explicit parameter: opponent, and the implicit parameter: this.

When we call:

tania.attack(sonya)

then

this = tania and opponent = sonya.

If we think of methods as object behaviors, then the implicit parameter is telling us who is exhibiting the behavior. Decrementing tania's health might produce a different effect than decrementing sonya's health. It could be the difference between life and death.

A function doesn't have an implicit parameter.

object GladiatorUtils {
  def damage(victim: Gladiator, amt: Int) =
     victim.health = math.max(0, this.health – amt)

  def attack(attacker: Gladiator, victim: Gladiator) =
     damage(attacker, 1)
     damage(victim, 5)
}

Mathematical functions like sin, exp, and gcd are functions.

We imitate functions in Scala with singleton methods as we did in GladiatorUtils. But to call these functions from outside we must provide the implicit parameter:

GladiatorUtils.attack(sonya, tania) // this = GladiatorUtils

Generic Functions

Most of the Scala collections are generic. For example, Array[T] is the type of all arrays of elements of type T, where T can be anything. Array methods simply don't care about T.

Notice the difference from Java where angle braces bracket types:

List<T> // Java

Generic functions/methods are type agnostic. Their algorithms don't depend on the type of their input.

We can define generic functions by making the parameter types itself be a parameter. Here's the syntax:

def id[T](x: T) = x // the identity function (which will be useful later)

Try implementing the identity function in a strongly typed language that doesn't allow generic functions.

For another example, here's a generic method for converting matricies (i.e., 2-dimensional arrays) into strings:

def toString[T](matrix: Array[Array[T]]) =
  var result = ""
  for(row <- matrix)
    for(elem <- row)
      result += elem.toString + " "
    result += '\n'
  result

Notice the way 2-dimensional arrays are declared. They're simply arrays of arrays of whatever. Our toString function doesn't care.

Here's some test code:

// here's the slick way we create a 3 x 4 2-dimensional array
val mat1 = Array.ofDim[Int](3, 4)
// we can use a fancy for-loop to fill it up
for(i <- 0 until 3; j <- 0 until 4) mat1(i)(j) = i + j
// and now we print it out
println(toString(mat1))

Here's the result:

0 1 2 3
1 2 3 4
2 3 4 5

Default Arguments

Scala methods can have default arguments:

def tax(income: Double, rate: Double = .05) =
  if (income < 2000) 0 else rate * income

tax(100000, .1) // = 10000
tax(100000)     // = 5000

Operator Overloading

Scala programmers can overload all of the operators: +, *, !, &&, etc.

class Money(val amt: Double) {
  if (amt < 0)
     throw IllegalArgumentException("No negative amounts")
  def +(other: Money) = Money(this.amt + other.amt)
  def -(other: Money) = Money(this.amt - other.amt)
  override def toString = amt.toString + " USD"
  // etc.
}

object TestMoney extends App {
  val cost1 = Money(32.25)
  val cost2 = Money(45.00)
  val payment = Money(50)
  val debt = cost1 + cost2 - payment
  println
("debt = " + debt) // prints "debt = 27.25 USD"
}

This works because:

cost1 + cost2

compiles to

cost1.+(cost2)

Calling Functions

Unary methods (i.e., methods with one explicit parameter) can be called using infix notation:

object Test extends App {
  val tania = Gladiator("Tania")
  val sonya = Gladiator("Sonya")
  tania attack sonya
  sonya
attack tania
 
// etc.
}

Which is better:

tania attack sonya

or what it compiles to:

tania.attack(sonya)

Objects as Functions

In functional programming functions represent algorithms that can be called and data that can be the inputs and outputs of functions. But the only kind of data in Scala are objects. The trick is that objects can be made to look like functions by giving them an apply method:

object numCalls {
  var nums = 0
  def apply() = { nums += 1; nums}
}

object TestNumCalls extends App {
  println("numCalls() = " + numCalls())
  println("numCalls() = " + numCalls())
  println("numCalls() = " + numCalls()))
}

Prints:

numCalls() = 1
numCalls() = 2
numCalls() = 3

Variable Arguments

A variable argument function can be called with any number of inputs. These inputs are collected together into an array that is the actual input:

def sumSquares(nums: Double*): Double =
  var result = 0.0
  for(num <- nums) result += square(num)
  result

println(sumSquares(1, 2, 3))  // 15
println(sumSquares(1, 2, 3, 4, 5)) // 55
println(sumSquares(1, 2, 3, 4, 5, 6, 7, 8, 9)) // 285