Functional jargon in Scala
Arity
Represents the number of parameters passed into a function.
More specific representations:
- A
nullary
function has an arity of zero - A
unary
function has an arity of one - A
binary
function has an arity of two - A
ternary
function has an arity of three - An
n-ary
function has an arity of n - A
variable arity
function takes variable number of parameters
// nullary
def time = System.nanoTime()
// unary
def addFive(x: Int): Int = x + 5
// binary
def sum(x: Int, y: Int): Int = x + y
// ternary
def ifElse[A](expression: => Boolean, trueOutcome: A, falseOutcome: A): A = if(expression) trueOutcome else falseOutcome
// variable arity
def sumAll(x: Int*): Int = x.foldLeft(0)(_ + _)
sumAll()
sumAll(1)
sumAll(1, 2, 3, 4)
Higher-Order Functions (HOF)
A higher order function is a function which takes another function as an argument and/or a function which returns another function as a result.
// map, flatMap and filter are examples of functions which take other functions as arguments
List(1, 2, 3).map(_ + 2)
List(1, 2, 3).flatMap(number => number.to(10).toList)
List(1, 2, 3).filter(number => (number % 2) == 0)
// generateAdder is an example of a function returning another function
def generateAdder(x: Int) = (y: Int) => y + x
val addFive = generateAdder(5)
addFive(4)
Currying
Currying transforms a function that takes multiple parameters into a chain of functions, each taking a subset of the parameters.
Currying is essentianly a factory for functions.
Its achieved by defining parameter lists (x:Int, t: Int)(y:Int)(z:Int)
.
The strange term currying is named after the logician Haskell Curry.
def sum(x:Int)(y: Int): Int = x + y
val sumFive = sum(5)(_)
println(sumFive(5))
Free and Bound variables
Free and bound variables are variables referenced in a function body.bound variables
are arguments to a function that are explicity defined in their function definition.free variables
are variables that are referenced in the function body and are defined outside the function
// bound variable function, x is a bound variable
def addOne(x: Int): Int = x + 1
object Counter {
var counter: Int = 0
// free variable function, counter is a free variable
def increment: Int = {
counter = counter + 1
counter
}
// mix of bound varibale `x` and free variable `counter`
def resetCounterTo(x: Int): Int = {
counter = x
counter
}
}
println(Counter.increment)
Closure
A function which has free variables bound are known as closures. Even if the scope of which the variable was defined has exited it will still exist if the closure function is within scope.
val increment = {
var counter = 0
() => {
counter = counter + 1
counter
}
}
println(increment())
Partial Application
A partially applied function is a function where some of the parameters are applied and it returns a function defined with just the unapplied parameters.
Underscore followed by its type _ : Int
is used to desiginate the unapplied parameters.
val enclose = (prefix:String, inner:String, postfix:String) => s"$prefix$inner$postfix"
val div = enclose("<div>", _ : String, "</div>")
val p = enclose("<p>", _ : String, "</p>")
println(div(p("Hello World!")))
Functor
Functors are types which define a map function. The map function adheres to two laws: preserve identity
and composablity
.
trait Functor[A] {
def map[B](f: A => B): Functor[B]
}
Preserve Identity
When an idenity functor is passed to a map. The resulting functor will be equal to the original functor.
// an identity function simply returns the argument passed in without changing it
def identity(x: Int) = x
List(1, 2, 3).map(identity) == List(1, 2, 3)
Composable
composing two functions and then mapping the resulting function over a functor should be the same as first mapping one function over the functor and then mapping the other one. functor.map(x => f(g(x))) = functor.map(g).map(f)
def addOne(x: Int) = x + 1
def multiplyByFive(x: Int) = x * 5
val nums = List(1, 2, 3)
nums.map(num => multiplyByFive(addOne(num))) == nums.map(addOne).map(multiplyByFive)
Examples of functors in the scala library are List
, Try
and Option
.
Referential Transparency
Is the ability to replace an expression by its resulting value and not change the behaviour of a program.
Referentially transparent
def add(x:Int, y: Int): Int = x + y
// this is referentially transparent
val sum = add(3, 4)
// and can be replaced with
val sum = 7
Not Referentially transparent
var counter = 0
def addAndCount(x:Int, y: Int): Int = {
counter = counter + 1
x + y
}
// this is not referentially transparent
val sum = addAndCount(3, 4)
val counterSum = _sum + counter
println(counterSum) // prints 8
counter = 0 // reset
// cannot be replaced with
val _sum = 7
val _counterSum = 7 + 0
println(_counterSum) // prints 7
Lambda
Lambda is an anonymous function that can be passed around like a value.
val sum = (x:Int, y:Int) => x + y
sum(3, 4)
They are often passed into Higher Order Functions
List(1, 2, 3)
.map((num: Int) => num * 10)
Algerbraic Data Types
ADT’s approach structuring your data as products
and sums
.
Sum types are this
OR that
Product types are this
AND that
As an example when modelling ADT’s for renewable power plants.
A power plant can be solar OR
wind.
A power source can be solar panels OR
turbines.
A power plant has many solar panels AND
a support contractor.
Solar
OR Wind
SolarPanel
OR Turbine
powerPanels
AND supportContractor
Example algerbraic data types in the core scala library Option
, Either
, Try
and List
sealed trait RenewablePlant {
val powerSources: Seq[PowerSource]
val supportContractor: String
}
sealed trait PowerSource
case class SolarPanel() extends PowerSource
case class Turbine() extends PowerSource
case class Solar(powerSources: Seq[SolarPanel], supportContractor: String) extends RenewablePlant
case class Wind(powerSources: Seq[Turbine], supportContractor: String) extends RenewablePlant
def identify(plant: RenewablePlant): Unit = plant match {
case Solar(_, _) => println("SUN SUN SUN")
case Wind(_, _) => println("WIND WIND WIND")
}
identify(Solar(List(), "Jim"))
identify(Wind(List(), "Mark"))
Value
A value is the result of an expression which can be assigned to a variable.
Examples of a value
// the lambda function here is a value
val add = (x:Int, y:Int) => x + y
// 3 is a value
val x = 3
Side Effect
A function is said to have a side effect if it read or wrote to external mutable state.
// no side effect, pure function
def add(x:Int, y:Int): Int = x + y
// side effects by printing
def addPrint(x:Int, y:Int): Int = {
println(x + y)
x + y
}
Pure
A pure function is said to have no side effects and is not allowed to read from any mutual external state.
// no side effect, pure function
def add(x:Int, y:Int): Int = x + y
Idempotent
An idempotent function can cause idempotent side-effects.
When calling an idempotent function multiple times with the same parameters, it will produce the same result and any of its side-effects will produce the same results.
The function below is idempotent, since the number stays removed from the set after subsequent calls i.e. additional calls do nothing.
var set = Set(1, 2)
def removeFromSet(x:Int): Boolean = {
set = set.-(x)
set.contains(x)
}
removeFromSet(2)
removeFromSet(2)
removeFromSet(2)
Constant
A constant is a variable which can not be reassigned once assigned.
In Scala a variable is made constant using the val
keyword.
Constants are referentially transparent.
val Five = 5
val sum = (x:Int, y:Int): Int => x + y
Semigroup
Formally, a type can be called a semigroup if it has:
- a combine operation with type (A, A) => A
- adheres to the rules of associatively when combining
Examples of semigroups are
- Concatenation of strings {"a" + "b"} + "c" == "a" + {"b" + "c"}
- Addition of integers {1 + 2} + 3 == 1 + {2 + 3}
- Oring of Booleans {true || false} || false == true || {false || false}
// def combine[A](a: A, b: A): A
def stringConcatSemigroup(a: String, b: String): String = s"$a$b"
def integerAddSemigroup(x: Int, y: Int): Int = x + y
def booleanAndSemigroup(a: Boolean, b: Boolean): Boolean = a && b
// assoicative laws held (x combine y) combine z = x combine (y combine z)
stringConcatSemigroup("x", stringConcatSemigroup("y", "z")) == stringConcatSemigroup(stringConcatSemigroup("x", "y"), "z")
integerAddSemigroup(1, integerAddSemigroup(2, 3)) == integerAddSemigroup(integerAddSemigroup(1, 2), 3)
booleanAndSemigroup(true, booleanAndSemigroup(false, true)) == booleanAndSemigroup(booleanAndSemigroup(true, false), true)
Lift
The term lifting
comes from category theory.
It is the ability to convert a function of A => B
to a lifted function F[A] => F[B]
where it can be applied to a functor or monad F[A]
A concrete example using the monad container Option
:
Assume we have a function that converts a string to an integer and the string is wrapped in an Option
monad. Here we cannot pass in the Option("4")
in to the function because of incompatiable types. So we have to lift the function to be compatiable.
def stringToInt(s: String): Int = s.toInt
val four: Option[String] = Option("4")
def lift[A, B](f: A => B): Option[A] => Option[B] = (a: Option[A]) => a.map(f)
val optionStringToInt = lift(stringToInt)
optionStringToInt(four)
Domain Codomain
A function maps one set, called a domain
, to another set, called the codomain
.
A function associates every element in the domain with exactly one element in the codomain. In Scala, both domain and codomain are known as types
.
// domain and codomain are both Ints
def increment(num: Int): Int = num + 1
// domain and codomain are Strings
def upperCase(s: String): String = s.toUpperCase
// domain is type Int and the codomain is type string
def intToString(num: Int): String = num.toString
Morphism
Is a mapping from type
to type
Endomorphism
A mapping where the input type is the same as the output type.
def upperCase(s: String): String = s.toUpperCase
def increment(num: Int): Int = num + 1
Isomorphism
A pair of transformations between 2 types of objects that is structural in nature and no data is lost.
For example, 2D coordinates could be stored as an array [2,3]
or object {x: 2, y: 3}
// Providing functions to convert in both directions makes them isomorphic.
case class Coords(x: Int, y: Int)
val pairToCoords = (pair: (Int, Int)) => Coords(pair._1, pair._2)
val coordsToPair = (coods: Coords) => (coods.x, coods.y)
coordsToPair(pairToCoords((1, 2)))
pairToCoords(coordsToPair(Coords(1, 2)))
Predicate
A predicate is a function which takes a value and returns a boolean (a:A):Boolean
Commonly used for filtering.something which is affirmed or denied concerning an argument of a proposition.
def isEven(a: Int):Boolean = (a % 2) == 0
Array(1, 3, 4, 5, 6).filter(isEven)
Auto Currying
Transforming a function that takes multiple arguments into one that if given less than its correct number of arguments returns a function that takes the rest. When the function gets the correct number of arguments it is then evaluated.
val add = (x: Int, y: Int) => x + y
val curriedAdd = add.curried
curriedAdd(2) // (y) => 2 + y
curriedAdd(1)(2)
Identity Function
An identity function takes one argument and returns that argument without modification.
There is an identity function defined in Scala Predef
def identity[A](a: A): a
List(List(1), List(2, 3), List(4, 5)).flatMap(identity) // List(1, 2, 3, 4, 5)
Monoid
A monoid is a Semigroup with an empty element, in math it is known as an identity element.
Formally, a monoid has the properties of a semigroup:
- a combine operation with type (A, A) => A
- adheres to the rules of associatively when combining
With the additional property:
- an empty element of type A
// def combine[A](a: A, b: A): A
val emptyString: String = ""
def stringConcatSemigroup(a: String, b: String = emptyString): String = s"$a$b"
val emptyAddInteger = 0
def integerAddSemigroup(x: Int, y: Int = emptyAddInteger): Int = x + y
val emptyAndBoolean = true
def booleanAndSemigroup(a: Boolean, b: Boolean = emptyAndBoolean): Boolean = a && b
// assoicative laws held (x combine y) combine z = x combine (y combine z)
stringConcatSemigroup("x", stringConcatSemigroup("y", "z")) == stringConcatSemigroup(stringConcatSemigroup("x", "y"), "z")
integerAddSemigroup(1, integerAddSemigroup(2, 3)) == integerAddSemigroup(integerAddSemigroup(1, 2), 3)
booleanAndSemigroup(true, booleanAndSemigroup(false, true)) == booleanAndSemigroup(booleanAndSemigroup(true, false), true)
Setoid
Is a type which can be compared with instances of the same type for equality.
Make array a setoid, therefore equivalent if sorted and has the same elements.
trait Eq[A] {
def eqv(X: A, y: A): Boolean
}
object Eq {
def apply[A](implicit ev: Eq[A]) = ev
implicit def arrayInstances[B]: Eq[Array[B]] = new Eq[Array[B]] {
def eqv(xs: Array[B], ys: Array[B]): Boolean =
xs.zip(ys).foldLeft(true) {
case (isEq, (x, y)) => isEq && x == y
}
}
implicit class EqOps[A](x:A) {
def eqv(y: A)(implicit ev: Eq[A]) = ev.eqv(x, y)
}
}
import Eq._
Array(1, 3) == Array(1, 3) // false
Array(1, 3).eqv(Array(1, 3)) // true
Array(1, 3).eqv(Array(0)) // false
Monad
“Eugenio Moggi first described the general use of monads to structure programs in 1991. Several people built on his work … early versions of Haskell used a problematic ‘lazy list’ model for I/O, and Haskell 1.3 introduced monads as a more flexible way to combine I/O with lazy evaluation.”
Monads are types which define a flatMap and pure function. The flatMap function adheres to three laws: left identity
, right identity
and associativity
.
trait Monad[A] {
def pure(value: A): Monad[A]
def flatMap[B](f: A => Monad[B]): Monad[B]
}
Examples of Monads in Scala are Option
and List
.
With Option and List the apply
function is the equivalent of pure
.
Left Identity
Calling pure on type A followed by a flatmapping over f is the same as calling f on type A
val a = 1
def f(x: Int): Option[Int] = Option.apply(x + 1)
Option.apply(a).flatMap(f) == f(a)
Right Identity
a monad flatmapping over pure results in the same monad
val a = 1
def f(x: Int): Option[Int] = Option.apply(x + 1)
val monad = Option.apply(a)
monad.flatMap(Option.apply) == monad
Associativity
flat mapping over function f and g is the same as flat mapping over f and then g.
val a = 1
def f(x: Int): Option[Int] = Option.apply(x + 1)
def g(x: Int): Option[Int] = Option.apply(x * 2)
val monad = Option.apply(a)
monad.flatMap(f).flatMap(g) == monad.flatMap(f(_).flatMap(g))
Immutability
Immutablility is a variable reference or object whose state cannot be modified after it is created.
Scala is not strict on immutability and allows mutable variables and mutable objects too.
- A reference can be declared immutable
val
or mutablevar
- Scala provides
immutable collection
andmutable collection
collection.immutable | collection.mutable | |
---|---|---|
val | 😃 | 😓 |
var | 😐 | 💩 |
// Order of preference
// 1 val --> immutable
val valImmutable = scala.collection.immutable.Set(0)
// 2 var --> immutable
var varImmutable = scala.collection.immutable.Set(0)
// 3 val --> mutable
val valMutable = scala.collection.mutable.Set(0)
// 4 var --> mutable
var varMutable = scala.collection.mutable.Set(0)
Equational Reasoning
Equational reasoning is enabled by Referential Transparency. When an application is composed of expressions which Scala can achieve and devoid of side effects, truths about the system can be derived from the parts.
Combinator
From the Haskell wiki “a style of oranising libraries centered around the idea of combining things”. Specifically in this case it is combining functions.
Scala has parser combinators which join parses.
def addOne(i: Int): Int = i + 1
def productTen(i: Int): Int = i + 10
// combining functions
List(1, 2, 3).map(addOne).map(productTen)
Category Theory
Samuel Eilenberg introduced the notion of category theory in 1942. At that point Saunders Mac Lane wrote the subject was called “general abstract nonsense”.
In 1945 they both defined the axioms for categories, functors and natural transformation.