Eq objects can be created through by
and instance
methods. They are useful to implement comparison methods for an object
case class Account(id: Int)
val eqIdBy: Eq[Account] = Eq.by(_.id)
implicit val eqIdInstance: Eq[Account] =
Eq.instance[Account]((a, b) => Eq[Int].eqv(a.id, b.id))
// testing
val a = Account(1)
val b = Account(2)
a === b // false
a.eqv(b) // false
Order objects are used to make comparisons between objects of same type and return a positive value when first > second, negative when first < second and 0 in case of both has the same value. Another ways to invoke Order implementations of an object are using compare, max and min methods. Order can be created using by
and from
methods.
case class Account(id: Int)
val orderByIdBy: Order[Account] = Order.by(_.id)
implicit val orderByIdFrom: Order[Account] =
Order.from((a, b) => Order.compare(a.id, b.id))
// testing
val a = Account(1)
val b = Account(2)
a compare b // -1
a min b // Account(1)
a max b // Account(2)
Show its a typeclass with a goal similar to toString. When you need multiple implementations of a string representation for an object, it can be useful. Different from Eq and Order, this typeclass can be implemented with Show.show
or Show.fromToString
.
case class Account(id: Int, owner: String, balance: Double)
val defaultShow: Show[Account] = Show.fromToString
implicit val showOwnerAndBalance: Show[Account] =
Show.show(account => s"${account.owner} -> $$${account.balance}")
val account = Account(1, "Leia", 1.9)
Show[Account].show(account) // Leia -> 1.9
account.show // Leia -> 1.9
defaultShow.show(account) // Account(1, Leia, 1.9)
Monoid its a typeclass with 2 methods to be implemented: empty
and combine
. In short, combine
provides a way to combine two instances of a same class and empty
represents a default instance of that object. For example, in a Int implementation of Monoid if we combine 1 and 2, we can have 3 as result. 0 can be the empty representation of Int.
case class Account(id: Int, owner: String, balance: Double)
object Account {
def mergeAccounts(a1: Account, a2: Account): Account =
a1.copy(balance=a1.balance + a2.balance)
implicit val combineAccount: Monoid[Account] =
Monoid.instance(Account(0, "none", 0), mergeAccounts)
}
val lukeAccount = Account(1, "Luke", 2.5)
val leiaAccount = Account(0, "Leia", 1000.5)
leiaAccount |+| lukeAccount // Account(0, "Leia", 1003.0)
List(leiaAccount, lukeAccount).combineAll // Account(0, "Leia", 1003.0)
leiaAccount.combine(lukeAccount) // Account(0, "Leia", 1003.0)
Functor trait provides a function called map
. Functor fits with a group of classes named High Kinded Typeclasses because its parametrized by more then one type. This function is parametrized by types A and B and receives a container of A and a function that transforms type A to B. Finally, the function returns a container of B. Its behavior is described by the following signature: map[A, B](fa: F[A])(f: A -> B): F[B]
.
val listFunctor: Functor[List] = new Functor[List] {
def map[A, B](fa: List[A])(f: A -> B): List[B] =
fa match {
case Nil => Nil
case head :: tail => f(head) :: map(tail)(f)
}
}
listFunctor.map(List(1, 2, 3))(_ + 1) // List(2, 3, 4)
listFunctor.as(List(1, 2, 3), 1) // List(1, 1, 1)
Applicative uses a concept of high kinded type in its implementation. The main goal of this typeclass is to allow the use methods like map to higher-kinded types. Kind its a classification saying how much type parameters a type requires. In this case, an ordinary type like String
or Int
has a proper kind represented as *
. Beyond that, derived types like List[Int]
has a kind of * -> *
. In other example, Either[Int, String]
has a kind of * -> * -> *
. This cases are called first-order kinds. After that, higher-kinded types represents a dependency of many degrees like in curried representation (* -> *) -> *
.
In Applicative, two methods needs to be implemented: pure and ap. All that its used to implement a different approach for a multi dimensional map. Pure its used to create a container of a received value. An Applicative has also an ap method. This method receives an container of functions from A to B and a container of A values to be transformed in a container of B.
val optionApplicative: Applicative[Option] = new Applicative[Option] {
def pure[A](x: A): Option[A] = Some(x)
def ap[A, B](ff: Option[A => B])(fa: Option[A]): Option[B] =
(ff, fa) match {
case (Some(f), Some(a)) => Some(f(a))
case _ => None
}
}
val stringOption = optionApplicative.pure("hello world") // Some(hello world)
val intOption = optionApplicative.pure(5) // Some(5)
val intOption2 = optionApplicative.pure(10) // Some(10)
(intOption, intOption2).mapN((a, b) => a + b) // Some(15)
(intOption, intOption2).map2((a, b) => a + b) // Some(15)
My extending Functor and Applicative, Monads are very similar to them. The main addition of a Monad is the flatMap
method. This method its similar to map
but,instead of receiving a function to other type (A => B
), it receives a function to a container (A => F[B]
). Of course, by extending Applicative it obligates the implementation of pure. Map
comes as an implementation of flatMap
and pure
.
trait MOption[+A]
object MOption {
case class MSome[A] extends MOption[A]
case object MNone extends MOption[Nothing]
implicit val monadMOption: Monad[MOption] = new Monad {
def pure[A](x: A): MOption[A] = MSome(x)
def flatMap[A, B](fa: MOption[A])(f: A => MOption[B]): MOption[B] =
fa match {
case MSome(value) => f(fa)
case MNone => MNone
}
}
}
In this case, the function can be implemented as the following example:
def map[A, B](fa: MOption[A])(f: A => B): MOption[B] =
flatMap(fa)(a => pure(f(a)))
A MonadError presents a bit more of power to a normal Monad: the ability to handle errors. So, beyond the pure
and flatMap
, MonadError requires the implementation of other two methods, handleErrorWith
and raiseError
.
implicit val tryME: MonadError[Try, Throwable] = new MonadError[Try, Throwable] {
def raiseError[A](e: Throwable): Try[A] = Failure(e)
def handleErrorWith[A](fa: Try[A])(f: Throwable => Try[A]): Try[A] =
fa match {
case Success(value) => Success(value)
case Failure(value) => f(a)
}
def pure[A](a: A): Try[A] = ???
def flatMap[A, B](fa: Try[A])(f: A => Try[B]): Try[B] = ???
}
So, in case we want to generalize an error handling for different kinded types (i.e. Try, Option, Either), we can use a signature similar to the following code. In this example, the type F[_]
its used to represent a kinded type and E
represents the error type of the MonadError. In addition, a function handling the error its used to transform an Exception
into an object of type E
(compatible with the MonadError signature).
trait HttpMethod
object GET extends HttpMethod
case class HttpRequest(method: HttpMethod, url: String)
case class HttpResponse(status: Int)
def executeRequest[F, E](req: HttpRequest)(f: Exception => E)(implicit ME: MonadError[F, E]): F[HttpResponse] =
try {
ME.pure(doRequest(req))
} catch {
case e: Exception => ME.raiseError(f(e))
}
type ErrorOn[A] = Either[String, A]
executeRequest[ErrorOn[A], String](HttpRequest(GET, "www.example.com"))((e: Exception) => e.getMessage())
MonadError provides other helper methods such as attempt
and ensure
. attempt
ensures that all non-fatal errors should be handled by this method. ensure
turns a value into an error if it doesn`t matches with a provided condition.
MonadError[Option, Unit].attempt(Some(3)) // Some(Right(3))
MonadError[Option, Unit].attempt(None) // Some(Left(())))
MonadError[Option, Unit].ensure(Some(3))(())(_ => _ % 2 == 0) // None
MonadError[Option, Unit].ensure(Some(2))(())(_ => _ % 2 == 0) // Some(2)
Foldable its a typeclass extending Monoid, so it inherits methods like empty
and combine
.
A Foldable implementation provides the methods foldRight
and foldLeft
. Both them receives a function to be executed recursively carrying an accumulator and the item to be iterated. foldRight
puts the recursion tree at right side by calling the function using the iteration value and the recursion stack as the result of the method. In other hand, foldLeft
does the opposite by executing the recursion using the empty value as an accumulator, so each iteration updates the accumulator for each recursion call. Because of these behaviors, foldLeft
iterates throw the container starting by the last item so depending of the function, the result will be reversed.
Recursion stack example for foldRight:
val f = (a, b) => a + b
val list = List(1, 2, 3)
val z = 0
foldRight(list, z)(f):
f(1, foldRight(List(2, 3), z)(f))
f(1, f(2, foldRight(List(3), z)(f)))
f(1, f(2, f(3, foldRight(List(3), z)(f))))
f(1, f(2, f(3, z)))
f(1, f(2, 3))
f(1, 5)
6
Recursion stack example for foldLeft:
val f = (a, b) => a + b
val list = List(1, 2, 3)
val z = 0
foldLeft(list, z)(f):
foldLeft(List(2, 3), f(z, 1))(f)
foldLeft(List(3), f(f(z, 1), 2))(f)
foldLeft(List(), f(f(f(z, 1), 2), 3))(f)
foldLeft(List(), f(f(1, 2), 3))(f)
foldLeft(List(), f(3, 3))(f)
6
Other useful methods from Foldable:
fold
: applies combine to the valuesfoldMap
: applies a function andfold
the result