Skip to content

Instantly share code, notes, and snippets.

@barambani
Last active December 31, 2019 22:33
Show Gist options
  • Save barambani/c74a29b2dbd225078be3f53ecdc7972c to your computer and use it in GitHub Desktop.
Save barambani/c74a29b2dbd225078be3f53ecdc7972c to your computer and use it in GitHub Desktop.
Applied Functional Programming In Scala Day 1
object Functions {
/**
* Functions
* - Total (every element in the domain must have a representation in the co-domain)
* - Deterministic (give always the same result for the same input)
* - Not side effecting (the only result of the function is the mapped value)
*/
/**
* This is not a function as is not total.
*
* - it throws if is not defined for the input
* - you cannot rely on the type signatures to know what's the behavior
*/
def square(x: Int): Int = x match {
case a if a > 0 => x * x
}
/**
* To make the above total
*/
def squareTotal(x: Int): Int = x * x
/**
* This is not a function as it is not deterministic.
*
* - they generate random bugs
* - hard to test
*/
def number(x: Int): Int =
if (Math.random() < 0.5) x * 2
else x * 3
/**
* To make the above deterministic
*/
def numberDeterministic(x: Int): Int =
if (x % 2 == 0) x * 2
else x * 3
/**
* This is not a function as it has side effects
*/
def triple(x: Int): Int = {
println("Something")
x * 3
}
/**
* To make the above pure
*/
def triplePure(x: Int): Int = x * 3
}
object EquationalThinking {
def x = scala.io.StdIn.readLine()
def replicate(s: String): String = s + s
replicate(x)
// Applying equational reasoning to the above
// step 1
replicate(scala.io.StdIn.readLine())
// step 2
scala.io.StdIn.readLine() + scala.io.StdIn.readLine()
// this is changing the meaning of the program
}
object HigherOrderFunctions {
/**
* Any time a function accepts or returns a function is called higher order function
*/
def filter(xs: List[Int])(f: Int => Boolean): List[Int] = ???
/**
* Either returns an error message or a parsed A and the remaining of the string
*/
type Parser[A] = String => Either[String, (A, String)]
val char: Parser[Char] =
(input: String) => input.headOption.map(char => Right((char, input.drop(1)))).getOrElse(Left("No more input!"))
def fail[A](error: String): Parser[A] =
_ => Left(error)
/**
* Function combinator: combines functions to get functions of the same shape
*/
def alt[A](l: Parser[A], r: Parser[A]): Parser[A] =
(input: String) => l(input) match {
case Left(_) => r(input)
case x => x
}
def seq[A, B](fa: Parser[A], f: A => Parser[B]): Parser[B] =
(input: String) => fa(input) match {
case Left(err) => Left(err)
case Right((a, remaining)) => f(a)(remaining)
}
def pure[A](a: A): Parser[A] =
(input: String) => Right(a, input)
def map[A, B](fa: Parser[A])(f: A => B): Parser[B] =
(input: String) => fa(input) match {
case Left(err) => Left(err)
case Right((a, remaining)) => Right((f(a), remaining))
}
def pipe[A, B, C](fst: A => B)(snd: B => C): A => C =
snd compose fst
/**
* Scala doesn't have polymorphic functions but has polymorphic methods
*
* scala methods accept type parameter list, and then a value parameter list, and then an implicit value parameter list
*/
def identity[A](a: A): A = a
// identity: A => (a: A) => A
// there is no type for the type-name A
def identity[A, B, C, D](a: A, b: B, c: C, d: D): A = ???
// identity: [A, B, C, D] => (a: A, b: B, c: C, d: D) => A = ???
/**
* Function literals or lambdas in Scala cannot be polymorphic
*/
val square: Int => Int =
(x: Int) => x * x
// val identity = ??? => A =
// (a: A) => a
/**
* A trick to workaround this is to create an object and exploit the apply method that,
* being a method, supports parametric polymorphism
*/
object identityPoly {
def apply[A](a: A): A = a
}
identityPoly(1)
identityPoly("foo")
identityPoly(true)
}
object AlgebraicDataTypes {
/**
* Product types
*
* given the types A and B (the terms of the product) they compose types productwise (A x B). They are tuples of all the
* possible combinations of an item from A and an item from B (a, b). The size is the size of A * the size of B
*
* in scala there are 2 idiomatic ways to represent product types:
*
* - case classes (named)
* - tuples (unnamed)
*/
final case class Person(name: String, age: Int)
type Person2 = (String, Int)
type Person3 = (String, Int, String, Float)
/**
* Sum types
*
* dual of product types. Given two types A and B (the terms of the sum), every element of the sum type is going
* to be a value from A or a value from B and the size is size of A + size of B
*
* in scala there are two ways to represent them but they are not first class citizen of the language.
*
* - sealed trait (named) [NB: If the trait is not sealed it's not a sum type]
* - either (unnamed)
*/
sealed trait Fruit
case object Apple extends Fruit
case object Banana extends Fruit
case object Mango extends Fruit
case object Grape extends Fruit
type Fruit2 = Either[Apple.type, Either[Banana.type, Either[Mango.type, Grape.type]]]
/**
* Example of ADT: Draft of GameWorld
*/
sealed trait Item
case class Player(
nickname: String,
inventory: List[Item],
level: Int,
position: (Int, Int)
)
sealed trait Cell
case class Land() extends Cell
case class Sea() extends Cell
case class GameMap(
world: Vector[Vector[Cell]]
)
case class GameWorld(
player: Player,
map: GameMap
)
}
object KindsAndTypeConstructors {
/**
* Considering again the structure of a method's signature, the type parameters list is not just a list
* of names, but is a list of names and kinds. In scala the syntax for describing the kinds is not consistent.
* The kind * is represented differently from all the others.
*
* I the case of identity below the kind of A is *. If we could express it consistently it would be written
*
* def identity[A : *](a: A): A = ???
*
* Anyway in Scala any time there is nothing after the name of a type in the type parameters list
* (like the identity below), a regular type of kind * has to be assumed
*/
def identity[A](a: A): A = ???
/**
* If we define something like this
*
* def filter[F, A](xs: F[A]): F[A] = ???
*
* to use it like
*
* filter[List, Int](1 :: 2 :: 3 :: 4 :: Nil)
*
* It will not work. It doesn't make sense to apply A to as with that definition we have
* set the kind of F to * (F : *). It is the same as writing
*
* val x = 2
* x(2)
*
* at value level. What we have to do instead of
*
* def filter[F : *, A : *](xs: F[A]): F[A] = ???
*
* is
*
* def filter[F : * => *, A : *](xs: F[A]): F[A] = ???
*
* In Scala there is no such a syntax, but the kind annotation can be used to express kinds different from *.
* In the example above using `F[_]` can be used for kinds * => * and omitting the annotation will
* define the kind of regular types. See below
*/
def filter[F[_], A](xs: F[A]): F[A] = ???
// and the following use will compiles
filter[List, Int](1 :: 2 :: 3 :: 4 :: Nil)
/**
* Example of kinds
*
* A : *
* Option : * => *
* List : * => *
* String : *
* Map : [*, *] => *
* Tuple2 : [*, *] => *
* Tuple4 : [*, *, *, *] => *
* Either : [*, *] => *
*/
trait StackLike[Stack[_]] {
def empty[A]: Stack[A]
def push[A](a: A, s: Stack[A]): Stack[A]
def pop[A](s: Stack[A]): Option[(A, Stack[A])]
}
/**
* Given the trait
*/
trait Algorithm[Stack[Stack]] {
def apply(implicit S: StackLike[Stack]): Stack[Int] = { S == S; ??? }
}
/**
* The kind of Algorithm is
*
* Algorithm : (* => *) => *
* (associated to the left)
*
*
* Given the trait below
*/
trait Module[Alg] {}
/**
* the following will not compile
* type MyModule = Module[Algorithm]
*
* to make it work we need to change Module to
*/
trait Module2[Alg[_[_]]] {}
type MyModule = Module2[Algorithm]
/**
* Where the kind of Module2 is
*
* Module2 : ((* => *) => *) => *
*/
}
object TypeLambdas {
/**
* If we consider a function like
*/
val plus: (Int, Int) => Int =
(x, y) => x + y
/**
* sometimes it's easier to be able to partially apply it
*/
val increment: Int => Int =
plus(1, _)
/**
* Let's consider something like the below that allows to fold over
* a structure
*/
trait Foldable[F[_]] {
def fold[Z, A](fa: F[A])(z: Z)(f: (Z, A) => Z): Z
}
val listFoldable = new Foldable[List] {
def fold[Z, A](fa: List[A])(z: Z)(f: (Z, A) => Z): Z =
fa.foldLeft(z)(f)
}
/**
* if we wanted to implement a foldable for Map
*
* val mapFoldable = new Foldable[Map] { [...] }
*
* it wouldn't work as the kinds don't align
*
* F : * => *
* Map : [*, *] => *
*
* to make it work I have to partially apply the kind. I could try
*
* def mapFoldable[K] = new Foldable[Map[K, _]] { [...] }
*
* But that doesn't work. A possible trick is to use an anonymous structural type
*
* ({ type Mpak[V] = Map[K, V] })#Mpak
*/
def mapFoldable[K] = new Foldable[({ type Mpak[V] = Map[K, V] })#Mpak] {
def fold[Z, A](fa: Map[K, A])(z: Z)(f: (Z, A) => Z): Z =
fa.values.foldLeft(z)(f)
}
/**
* It's very hard to read and error-prone. A possible solution is using a compiler plugin
* called Kind Projector. With it the syntax becomes very elegant anc concise.
* The above could be expressed
*/
def mapFoldable2[K]: Foldable[Map[K, ?]] = new Foldable[Map[K, ?]] {
def fold[Z, A](fa: Map[K, A])(z: Z)(f: (Z, A) => Z): Z =
fa.values.foldLeft(z)(f)
}
/**
* Another way would be using type aliases to explicitly partially apply the type
*/
def mapFoldable3[K]{
type MapK[V] = Map[K, V]
new Foldable[MapK] {
def fold[Z, A](fa: Map[K, A])(z: Z)(f: (Z, A) => Z): Z =
fa.values.foldLeft(z)(f)
}
}
}
object PolymorphismAndTypeclasses {
/**
* considering a method with the signature below (fst) there is only a possible
* way to implement it, and it's the way given. If we assume purity there's no
* other way.
*/
def fst[A, B](tuple: (A, B)): A = tuple._1
trait Appendable[A] {
def append(a: A, b: A): A
}
def foo[A](x: A)(implicit A: Appendable[A]): A = ???
/**
* If we try the same with foo instead, possible implementations could be
*
* A.append(x, x)
* A.append(A.append(x, x), x)
*
* still a lot of possible ways but they are described by the type signature
*
* Polymorphism restricts the possible implementations, and even if is not
* reducing to a single possible implementation still describes well what the class of
* the implementations could do
*/
def zip1[A](a: List[A], b: List[A]): List[(A, A)] = ???
/**
* The above can be still ambiguous. For instance we don't know if the items from the first list
* go first or second in the result. Or even if this changes with the position.
* With the following is impossible to invert the order in the reult tuples
*/
def zip2[A, B](a: List[A], b: List[B]): List[(A, B)] = ???
/**
* There is still a problem because we don't know what to do with the extra items
* if the lists are not the same size. A lot of details can be taken off. The steps
* below demonstrates a possible way
*/
def zip3[A, B, C](a: List[A], b: List[B], f: Either[B, (A, B)] => C): List[C] = ???
def zip4[A, B, C](a: List[A], b: List[B], f: Either[B, Either[A, (A, B)]] => C)(zero: C, smash: (C, C) => C): C = ???
def zip5[A, B, C : Monoid](a: List[A], b: List[B], f: A \&/ B): C = ???
def zip6[F[_] : Apply, A, B, C : Monoid](a: F[A], b: F[B], f: A \&/ B): C = ???
/**
* Sometimes we need to know some structure to know how to implement something.
* The purpose is to introduce the minimum amount of structure to write an implementation.
* We don't want too introduce too much structured though as the space of the possible implementations
* explodes otherwise. We can do it with type classes
*
* Below we would like to say that we can compare two `A`s but we cannot do it with that signature
*/
def sort0[A](list: List[A]): List[A] = ???
/**
* Type classes are not intended as traits in object oriented. They are a different
* usage of the `trait` construct
*/
sealed trait Ordering
case object LT extends Ordering
case object GT extends Ordering
case object ET extends Ordering
/**
* A Typeclass is a bundle of types and functions
*/
trait Ord[A] {
def compare(l: A, r: A): Ordering
}
/**
* The instances of a Typeclass provide materialization of it for different types
*/
val intOrd = new Ord[Int] {
def compare(l: Int, r: Int): Ordering =
if(l < r) LT
else if (l > r) GT
else ET
}
/**
* This type class describes things that can be compared. Is the minimum structure we need
* to know how to sort items from A.
*
* Now we can implement sort
*/
def sort[A](list: List[A])(ord: Ord[A]): List[A] =
list match {
case Nil => Nil
case x :: xs =>
val (lessThan, notLessThan) = xs.partition(a => ord.compare(x, a) == LT)
sort(lessThan)(ord) ++ (x :: Nil) ++ sort(notLessThan)(ord)
}
sort(1 :: 2 :: 3 :: Nil)(intOrd)
/**
* some syntactic help can here can come from implicits
*/
implicit val intOrd2 = new Ord[Int] {
def compare(l: Int, r: Int): Ordering =
if(l < r) LT
else if (l > r) GT
else ET
}
def sort1[A : Ord](list: List[A]): List[A] =
list match {
case Nil => Nil
case x :: xs =>
val (lessThan, notLessThan) = xs.partition(a => implicitly[Ord[A]].compare(x, a) == LT)
sort1(lessThan) ++ (x :: Nil) ++ sort1(notLessThan)
}
/**
* using implicitly is ugly, it's better to use an helper.
* Adding this apply to the companion object of the typeclass will allow a nicer syntax
*/
object Ord {
def apply[A](implicit inst: Ord[A]): Ord[A] = inst
}
/**
* So the above becomes
*/
def sort2[A : Ord](list: List[A]): List[A] =
list match {
case Nil => Nil
case x :: xs =>
val (lessThan, notLessThan) = xs.partition(a => Ord[A].compare(x, a) == LT)
sort2(lessThan) ++ (x :: Nil) ++ sort2(notLessThan)
}
/**
* Considering the typeclass structure we will probably will have to decide where to put the instance.
*
* A good set of rules is:
*
* - If we are defining an instance of a type that we don't control (such as for Int) -> we should put the instance
* in the companion object of the type class. Scalac will find it in scope (called implicit scope)
*
* - If we are defining our own type (like Room below) -> we should put the instance in the companion object
* of the type we define and control.
*
* Anyway, is important not to have orphan instances at all, so not to have more than one possible instance of
* the typeclass for a specific type, that's selected through import. The problem here is that in case
* of orphan instances, changing an import we can brake a program and the type system would not identify
* the problem.
*/
case class Room(name: String, number: Int)
object Room {
implicit val intOrd2 = new Ord[Room] {
def compare(l: Room, r: Room): Ordering =
if(l.name < r.name) LT
else if (l.name > r.name) GT
else {
if(l.number < r.number) LT
else if (l.number > l.number) GT
else ET
}
}
}
/**
* If we really need to have more than one instance for the same type (we shouldn't typeclasses should be coherent),
* we should use newtypes, so that the user can select the which instance is summoning and doesn't delegate
* this to the Scala implicit resolution. This way even changing an import the behavior of the program will not change.
*/
class ReverseRoom(val room: Room) extends AnyVal
object ReverseRoom {
// Instances for this new type go here
}
/**
* Sometimes we might want to have an object oriented style syntax (call the method using the dot notation
* from the type instance). In this case there's a pattern to create that
*/
implicit class OrdSyntax[A](l: A) {
def =?=(r: A)(implicit O: Ord[A]): Ordering = O.compare(l, r)
}
/**
* So I can write the sort using the syntax (x =?= a) == LT
*/
def sort3[A : Ord](list: List[A]): List[A] =
list match {
case Nil => Nil
case x :: xs =>
val (lessThan, notLessThan) = xs.partition(a => (x =?= a) == LT)
sort3(lessThan) ++ (x :: Nil) ++ sort3(notLessThan)
}
/**
* The reason why typeclasses are more expressive than subtyping is that you can define instances
* (so functionality, behaviors) for types that you don't control
*
* In cases when to define an instance we need to summon anohter instance (the instance depends on another one)
* we can do that like below (see A : Ord)
*/
implicit def ordList[A : Ord] = new Ord[List[A]] {
def compare(l: List[A], r: List[A]): Ordering =
(l, r) match {
case (Nil, Nil) => ET
case (Nil, _) => LT
case (_, Nil) => GT
case (x :: xs, y :: ys) =>
x =?= y match {
case LT => LT
case GT => GT
case ET => compare(xs, ys)
}
}
}
(1 :: 2 :: 3 :: 4 :: Nil) =?= (5 :: 6 :: 7 :: 8 :: Nil)
/**
* Typeclasses usually come with laws. They define what's needed to have valid instances of a typeclass.
* The laws constraint the behavior that can be expected by the typeclass. If an instance doesn't satisfy the laws
* we cannot expect for that instance to have the behavior that the typeclass describes.
*
* Let's consider examples where the laws are in the comments
*/
trait Semigroup[A] {
// associativity law: append(append(a, b), c) == append(a, append(b, c))
def append(a1: A, a2: A): A
}
trait Monoid[A] extends Semigroup[A] {
// identity law:
// append(zero, a) == a
// append(a, zero) == a
def zero: A
}
object Monoid {
def apply[A](implicit I: Monoid[A]): Monoid[A] = I
}
/**
* without laws we cannot completely define a typeclass' behavior. With the traits above, we could define
* all the operations for a type, but if the associativity or the identity are not satisfied we couldn't call
* them semigroup or monoid instances
*/
implicit val intMonoid = new Monoid[Int] {
def zero: Int = 0
def append(a1: Int, a2: Int): Int = a1 + a2
}
/**
* Now if we want to create another monoid we don't want to create another instance for the same type.
* As said earlier we will use newtypes
*/
class MultInt(val value: Int) extends AnyVal
implicit val multIntMonoid = new Monoid[MultInt] {
def zero: MultInt = new MultInt(1)
def append(a1: MultInt, a2: MultInt): MultInt =
new MultInt(a1.value * a2.value)
}
def reduce[A: Monoid](list: List[A]): A =
list match {
case Nil => Monoid[A].zero
case x :: xs => Monoid[A].append(x, reduce(xs))
}
/**
* This adds a bit of verbosity but the types are sound in the sense that
* it will allow us to chose the monoid and will not delegate it to the implicit resolution
*
* A possible way to use it is below.
*/
reduce(1 :: 2 :: 3 :: Nil) // Summons Int Monoid
reduce(
(1 :: 2 :: 3 :: Nil).map(new MultInt(_)) // Summons MultInt Monoid
)
}
object TypeclassExercise {
import AlgebraicDataTypes._
case class FruitBlend(l: List[Fruit])
trait Blender[A, B] {
// blend(a, blend(b, c)) == blend(blend(a, b), c)
// blend(a, b) == blend(b, a)
def blend(a: A, b: B): FruitBlend
}
object Blender {
def apply[A <: Fruit, B <: Fruit](implicit A: Blender[A, B]): Blender[A, B] = A
implicit def appleBlend[B <: Fruit]: Blender[Apple.type, B] =
new Blender[Apple.type , B] {
def blend(a: Apple.type, b: B): FruitBlend = FruitBlend(a :: b :: Nil)
}
}
implicit class BlenderSyntax[A <: Fruit](a: A) {
def blend[B <: Fruit](b: B)(implicit BL: Blender[A, B]) = BL.blend(a, b)
}
/**
* Another example
*/
trait Navigatable[A] {
def goSouth(a: A): Option[A]
def goNorth(a: A): Option[A]
def goEast(a: A): Option[A]
def goWest(a: A): Option[A]
// laws:
// goSouth(a0).flatMap(a => gotNorth(a) == Some(a)).orElse(true)
// ... and so on
}
}
object Functor {
case class Identity[A](run: A)
case class Empty[A]()
trait Functor[F[_]] {
def fmap[A, B](f: A => B): F[A] => F[B]
}
object Functor {
implicit val listFunctor = new Functor[List] {
def fmap[A, B](f: A => B): List[A] => List[B] = _ map f
}
implicit def funcFunction[X] = {
type FunctionF[B] = X => B
new Functor[FunctionF] {
def fmap[A, B](f: A => B): (X => A) => (X => B) =
ff => f compose ff
}
}
implicit val identityFunctor = new Functor[Identity] {
def fmap[A, B](f: A => B): Identity[A] => Identity[B] =
ff => Identity(f(ff.run))
}
}
case class StreamT[F[_], A](run: F[(A, Option[StreamT[F, A]])])
}
object NaturalTransformation {
trait NaturalTransformation[F[_], G[_]] {
def apply[A](fa: F[A]): G[A]
}
type ~>[F[_], G[_]] = NaturalTransformation[F, G]
object NaturalTransformation {
def apply[F[_], G[_]](implicit INST: F ~> G): F ~> G = INST
implicit val listToOption: List ~> Option = new NaturalTransformation[List, Option] {
def apply[A](fa: List[A]): Option[A] = fa.headOption
}
implicit val optionToList: Option ~> List = new NaturalTransformation[Option, List] {
def apply[A](fa: Option[A]): List[A] =
fa.fold(Nil: List[A])(List(_))
}
}
}
object Applicative {
import Functor._
trait Apply[F[_]] extends Functor[F] {
// F[A => A] => F[A] => F[B]
def ap[A, B](fab: F[A => B]): F[A] => F[B]
// ap in term of zip
def ap1[A, B](fab: F[A => B]): F[A] => F[B] =
fa => fmap((t: (A => B, A)) => t._1(t._2))(zip(fab, fa))
def zip[A, B](fa: F[A], fb: F[B]): F[(A, B)] =
ap(fmap((a: A) => (b: B) => (a, b))(fa))(fb)
}
object Apply {
implicit val identityApply = new Apply[Identity] {
def ap[A, B](fab: Identity[A => B]): Identity[A] => Identity[B] =
fa => Identity(fab.run(fa.run))
def fmap[A, B](f: A => B): Identity[A] => Identity[B] =
ff => Identity(f(ff.run))
}
implicit val optionApply = new Apply[Option] {
def ap[A, B](fab: Option[A => B]): Option[A] => Option[B] =
fa => (fab, fa) match {
case (Some(ab), Some(a)) => Some(ab(a))
case _ => None
}
def fmap[A, B](f: A => B): Option[A] => Option[B] = _ map f
}
}
trait Applicative[F[_]] extends Apply[F] {
def point[A](a: A): F[A]
}
trait Monad[F[_]] extends Applicative[F] {
def join[A](ffa: F[F[A]]): F[A]
def bind[A, B](fa: F[A])(f: A => F[B]): F[B] =
join(fmap(f)(fa))
}
}
object examplesMonadTransformers {
lazy val result1: Option[Int] =
for {
a <- Option(1)
b <- if (a == 2) result1 else Option(2)
} yield a + b
/**
* this will be translated to
*/
val result2: Option[Int] =
Option(1).flatMap(a =>
Option(2).map(b =>
a + b
)
)
/**
* We have to respect the functor inside the for. If we want to do other operations we can use the
* variable definition
*/
val result3: Option[Int] =
for {
a <- Option(1)
b = if (a == 2) 4 else 5
} yield a + b
/**
* If we write something like
*
* val result4: Option[Int] =
* for {
* a <- Option(1)
* b = List(1, 2, 3)
* } yield a + b
*
* it won't compile, as we're trying to join different monad and that's not allowed by the types (see map).
* So what if we need to handle something like the below
*/
def f[A]: Future[Option[List[A]]] = ???
/**
* To work with that would require a lot of nested map and flatMap. That's because Monads don't compose
* as they have a lot of structure (so they are powerful) and they lose flexibility.
* If compared to Functors for instance, they can describe much more powerful behavior than simple mapping but
* they are more constrained in the structure and lose the possibility of composition (that instead Functor has).
*
* The way to stack Monads as they don't compose is monad transformers
*/
import Functor._
final case class OptionT[F[_], A](run: F[Option[A]]) {
def map[B](f: A => B)(implicit F: Functor[F]): OptionT[F, B] =
OptionT(
F.fmap((v: Option[A]) => v.map(a => f(a)))(run)
)
def flatMap[B](f: A => OptionT[F, B])(implicit F: Monad[F]): OptionT[F, B] =
OptionT(
F.bind(run)((v: Option[A]) => v match {
case None => F.point(Option.empty[B])
case Some(a) => f(a).run
})
)
}
object OptionT {
def point[F[_], A](a: A)(implicit F: Monad[F]): OptionT[F, A] =
OptionT(F.point(Some(a)))
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment