def functionName(pName1: pType1, pName2: pType2): ReturnType = implementation
A pure function is one where the same inputs always produce the same output. By definition pure functions do not have side effects.
A function has a side effect if it does something other than simply return a result. For example:
- Modifying a global variable
- Mutating a field or parameter
- Throwing an exception or halting with an error
- Printing to the console or reading user input
- Reading from or writing to a file
- Drawing on the screen
In all the above scenarios, actions within the side-effecting function are visible and affect code that is external to the function.
An expression is referentially transparent if all instances of it can be replaced with the result of a single invocation without changing the behaviour of the overall program. Referencial transparency holds when only pure functions are used.
A partial function is a function for which every input does not have a output. For example converting a String to a Number. Not all Strings are Numbers. This "gap" leads to the use of constructs like Exception and null. Inherently partial functions are harder to reason about than total functions.
A total function is a function for which every input has an output. For example converting from a Number to a String. Every Number can be converted to a String.
Converting a function with multiple arguments into a function with a single argument that returns another function.
def f(a: Int, b: Int): Int // uncurried version (type is (Int, Int) => Int)
def f(a: Int)(b: Int): Int // curried version (type is Int => Int => Int)
A higher order function is a function that can:
- Take one or more functions as arguments
- Return a function as its result
The Scala List is an immutable recursive data structure. It is a linked list which is built from “cons” cells ::
and ends in a Nil
element.
sealed trait List[+A]
case class ::[A](head: A, tail: List[A]) extends List[A]
case object Nil extends List[Nothing]
You can create a list in different ways:
val list1 = List(1,2,3)
val list2 = 1 :: 2 :: 3 :: Nil
val list3 = ::(1, ::(2, ::(3, Nil)))
The first argument of foldLeft is a seed value B
to be used for the first element, and the second argument is the function to apply.
def foldLeft[B](z: B)(op: (B, A) ⇒ B): B
Example:
List(1, 2, 3).foldLeft(20)(_ + _)
// result = 26
List(1, 2, 3).foldLeft(100)(_ + _)
// result = 106
The first argument of foldRight is a seed value B
to be used for the first element, and the second argument is the function to apply.
def foldRight[B](z: B)(op: (A, B) ⇒ B): B
Example:
List(1, 2, 3).foldRight(0)(_ + _)
// result = 6
List(1, 2, 3).foldRight(100)(_ + _)
// result = 106
foldLeft
starts on the left side (the first item) and iterates to the right; foldRight
starts on the right side (the last item) and iterates to the left.
val list: List[Int] = List(1, 3, 5, 7, 9)
list.foldLeft(0)(_ + _)
// This is the only valid order
0 + 1 = 1
1 + 3 = 4
4 + 5 = 9
9 + 7 = 16
16 + 9 = 25 // done
list.foldRight(0)(_ + _)
// This is the only valid order
1 + (3 + (5 + (7 + (9 + 0))))
1 + (3 + (5 + (7 + 9)))
1 + (3 + (5 + 16))
1 + (3 + 21)
1 + 24
25 // done
ADTs define a fixed set of all possible values of a given type. Two common classes of algebraic types are product types AND
and sum types OR
.
For sum types, the number of all its possible values is the sum or disjoint union of the number of all values of the two underlying types.
The values of a product type typically contain several values, called fields. For products types, the number of its possible values is the product of the fields.
Values of algebraic types are analysed with pattern matching, which identifies a value by its constructor or field names and extracts the data it contains.
When defining an algebraic data type using enums, it allows the compiler to exhaustively check the possible cases in match expressions. The compiler will emit a warning (or an error is the option "-Xfatal-warnings" option is enabled) if you have missed a specific case. The compiler knows all of the subtypes of the trait that can possibly exist as they can only be extended in the file.
enum MyBooleanType {
case True, False
}
Pattern matching is a mechanism for checking a value against a pattern. Pattern matching is similar to a series of if/else statements or a switch statement in other languages.
Pattern matching on an Int:
def numToString(num: Int): String =
num match {
case 0 => "zero"
case 1 => "one"
case 2 => "two"
case _ => "many"
}
Pattern matching on an Array:
def onlyThree(values: String): String =
values.split(",") match {
case Array(one, two, three) => s"Got 3: $one, $two, $three"
case _ => s"I need three things"
}
onlyThree("one,two,three")
> Got one, two, three
onlyThree("one")
> I need three things
The .map function on a List applies a function to each element in the List. It works on other data types as well (e.g. Option, Either, etc).
def map[A,B](fa: F[A])(f: A => B): F[B]
def map[A,B](oa: Option[A])(f: A => B): Option[B]
def map[A,B](ea: Either[A])(f: A => B): Either[B]
def map[A,B](ta: Try[A])(f: A => B): Try[B]
flatMap works by applying a function that returns a container type (e.g. a List, Option, Either, etc.) for each element within the container, and flattening the results into a value of the same container type.
flatMap[A,B](fa: F[A])(f: A => F[B]): F[B]
flatMap[A,B](oa: Option[A])(f: A => Option[B]): Option[B]
flatMap[A,B](ea: Either[A])(f: A => Either[B]): Either[B]
flatMap[A,B](ta: Try[A])(f: A => Try[B]): Try[B]
A for-comprehension is syntactic sugar for map, flatMap and filter operations on collections.
The general form is for {s} yield e
s
is a sequence of generators and filters
p <- e
is a generator
For example:
for {
x <- Some(3)
y <- Some(4)
} yield (x + y)
// result = Some(7)
is equivalent to:
Some(3).flatMap(x => Some(4).map(y => x + y))
The Option data type has two cases: it can be defined (Some), or it can be undefined (None).
sealed trait Option[+A]
case class Some[+A](get: A) extends Option[A]
case object None extends Option[Nothing]
Example:
def mean(xs: Seq[Double]): Option[Double] =
if (xs.isEmpty) None
else Some(xs.sum / xs.length)
The Either data type has only two cases which both carry a value. Either represents values that can be one of two things.
When we use it to indicate success or failure, by convention the Right
constructor is reserved for the success case and Left
is used for failure.
sealed trait Either[+E, +A]
case class Left[+E](value: E) extends Either[E, Nothing]
case class Right[+A](value: A) extends Either[Nothing, A]
Example:
def mean(xs: IndexedSeq[Double]): Either[String, Double] =
if (xs.isEmpty) Left("mean of empty list!")
else Right(xs.sum / xs.length)
The Try type represents a computation that may either result in an exception, or return a successfully computed value
def handleAttempt[A](call: Try[A]) = {
call match {
case Success(i) => println("Got: " + i)
case Failure(ex) => println("Failed: " + ex.getMessage)
}
}
Data Type | Definition |
---|---|
Boolean | true or false |
Byte | 8-bit signed two's complement integer (-2^7 to 2^7-1, inclusive) -128 to 127 |
Short | 16-bit signed two's complement integer (-2^15 to 2^15-1, inclusive) 32,768 to 32,767 |
Int | 32-bit two's complement integer (-2^31 to 2^31-1, inclusive) 2,147,483,648 to 2,147,483,647 |
Long | 64-bit two's complement integer (-2^63 to 2^63-1, inclusive) -9,223,372,036,854,775,808 to +9,223,372,036,854,775,807 |
Float | 32-bit IEEE 754 single-precision float 1.40129846432481707e-45 to 3.40282346638528860e+38 (positive or negative) |
Double | 64-bit IEEE 754 double-precision float 4.94065645841246544e-324d to 1.79769313486231570e+308d (positive or negative) |
Char | 16-bit unsigned Unicode character (0 to 2^16-1, inclusive) 0 to 65,535 |
String | a sequence of Chars |
Referenced from alvinalexander.com
class - A basic construction unit that contains fields, methods and other top-level types like objects, classes and traits.
object - An object is a class that has exactly one instance.
case class - Is a class that provides the following features:
- instantiated without the new operator
- is immutable
- provides access to all constructor fields
- provides a toString implementation
- overrides equals and hashCode based on the constructor fields
- supports pattern matching
- provides a copy function create new instances with the fields changed
case object - An object that can be pattern matched on.
trait - Is similar to an interface but also supports function implementations.
A companion object is an object with the same name as a class or trait and is defined in the same source file.
class MyNumber(n: Int) {
// body
}
object MyNumber {
//body
}
Companion objects are commonly used for "factory" or "smart constructors", via Scala's apply function e.g.:
object MyNumber {
def apply(strNumber: String): Option[MyNumber] = ???
}
Then use the smart constructor:
val maybeNumber = MyNumber.apply("123")
Or in its shorthand mode (the apply function is the default function on any object):
val maybeNumber = MyNumber("123") //
Side note: When we execute a function, we're actually invoking its apply
method.
They are typeclasses that define certain behaviour. Data types (List
, Option
, etc.) that have these typeclass instances are able to perform these behaviours.
The behaviour that Functor provides is the ability to map
. This means if you have a function A => B
, you can get a function F[A] => F[B]
provided that F
has the Functor typeclass instance.
More concretely, if there is a function getListingDesc: Listing => ListingDescription
, we get all these functions below for free (by using .map(getListingDesc)
):
List[Listing] => List[ListingDescription]
Vector[Listing] => Vector[ListingDescription]
Either[AppError, Listing] => Either[AppError, ListingDescription]
IO[Listing] => IO[ListingDescription]
- etc.
Because List
, Vector
, Either[AppError, ?]
and IO
have Functor instances.
In addition to whatever Functor provides, Applicative provides us the ability to map
over multiple things. If you have a function (A, B, C, ...) => Z
, you get a function (F[A], F[B], F[C], ...) => F[Z]
provided that F
has an Applicative typeclass instance.
More concretely, given a function createPerson: (FirstName, LastName, Age) => Person
, we get the following functions for free (by using .mapN(createPerson)
):
(IO[FirstName], IO[LastName], IO[Age])
=>IO[Person]
(ProvidedFirstName
,LastName
andAge
in anIO
, construct aPerson
if all 3IO
operations succeed, otherwise return the first error)(Option[FirstName], Option[LastName], Option[Age]) => Option[Person]
(ProvidedFirstName
,LastName
andAge
that may or may not exist, construct aPerson
if they all exist, otherwiseNone
)- etc.
Secondly, Applicative also provides us a way to put a value into the data type, i.e. a function pure: A => F[A]
, where F
has an Applicative instance.
This allows us to put a String
into a List
to get a List[String]
or an Int
into an IOto get an
IO[Int]`.
In addition to everything Applicative provides, Monad provides the ability to flatten nested structures, i.e. a function flatten: F[F[A]] => F[A]
. For instance, we can flatten Option[Option[Person]]
into Option[Person]
and IO[IO[String]]
to IO[String]
.
Why is this useful?
Because sometimes when we use .map
, we end up with nested structures, which can be redundant and very difficult to work with. flatten
keeps our type signatures clean.
Consequently, we also have a function flatMap
that does map
and then flatten
for this purpose.