For a developer to be able to work with Functional Programming, there are a few concepts they must master.
In addition to the functional constraints, there are two very important main ideas: algebraic data types (ADT) and algebraic structures. In this post, we’re looking at some algebraic structures.
Functor
Functor is the simplest algebraic structure. It’s a wrapper around data that can map operations over them and their successful results.
A basic functor example in Scala is scala.util.Try
:
Try(1 / x)
.map(e => e*e)
.map(1 - _)
.map(_.toString) match {
case Success(value) => s"the result is $value"
case Failure(exc) => s"it raised an exception: $exc"
}
Most of the Scala basic wrappers, like Seq
/List
, Option
, Either
, etc, are functors too.
Monad
Haskell’s monads have been getting people in dread, but they’re nothing than a variation of functor approach.
A monad is a wrapper around data that can operate over them flatly mapping the results and possibly describing side-effects.
Our first monad example is the Seq
:
Seq(1, 2, 3)
.flapMap(e => Seq(e * 2))
.flatMap(e => if (e == 2) Nil else Seq(e))
.flatMap(e => if (e == 3) Seq(4, 5) else Seq(e))
The result is Seq(4, 5, 6)
; the flatMap
method is the main resource of a monad.
Since Seq
is a functor too, you may replace the first flatMap
by a map
, and it has a filter
method, more expressive than flatMap
:
Seq(1, 2, 3)
.map(_ * 2)
.filter(_ != 2)
.flatMap(e => if (e == 4) Seq(4, 5) else Seq(e))
The result is the same. List
, Option
, Try
, Either
, etc, are monads too.
The knownest monad is the Haskell’s IO
. The IO
monad describes how an I/O side-effect happens.
For example:
greetings :: IO ()
greetings = printStrLn "Hello, World!"
The greetings
function idempotently returns an IO
monad that can trigger a writing to the standard output.
Monoid
A monoid is a structure that supplies an operation and a unit instance for that operation and type.
The monoid definition is:
trait Monoid[A] {
def op(a: A, b: A): A
def unit[A]: A
}
Let’s, for instance, define two monoids for dealing with integer and string sequences:
given object IntMonoid extends Monoid[Int] {
def op(a: Int, b: Int): Int = a + b
def unit: Int = 0
}
given object StringMonoid extends Monoid[String] {
def op(a: String, b: String): String = a concat b
def unit: String = ""
}
Defined these two monoids, we can code a function that can reduce an integer or string sequence:
def reduce[A](xs: Seq[A])(using ev: Monoid[A]): A =
if (xs.isEmpty) ev.unit
else ev.op(xs.head, reduce(xs.tail))
It works like this:
reduce(Seq(1, 2, 3)) // returns 6
reduce(Seq("Hello", ", ", "World", "!")) // returns "Hello, World!"
In order for the reduce
function to work properly with other types, it’s enough to supply another type’s implicit monoid, for instance a DoubleMonoid
, no need for changing the reduce
function.
Apply, applicative, semiring, lattice, etc
Other algebraic structures are mostly just variants of these three we’ve just been through. You can find some definitions in this glossary.
Original post in Kodumaro.
Top comments (1)
I forgot to mention, an example of Scala’s built-in monoid is Numeric: the addition method is
ev.plus
, and the unit isev.zero
.