ScalaFP: Understanding Semigroups In Scala Pragmatically

Reading Time: 3 minutes

In our previous post, we discussed semigroups according to mathematics and we conclude that semigroups have two properties called closure and associativity. But still, we have some questions like:

  1. How can we use semigroups by using Scala?

  2. Where do we require to use semigroups?

First, let’s try to figure out, when and where we require semigroups in our code and during this we will automatically figure out, how can we use semigroups as well. First of all, we have an example which is divided into steps and during this example, we are going to explore semigroups pragmatically.

Pre-requisite:

case class Money(dollars: Int, cents: Int)
trait Data {
val balance = Money(102, 44)
val salary = Money(320, 0)
val balances: Map[String, Money] = Map(
"James" > Money(212, 98),
"Jimmy" > Money(43, 44)
)
val salaries: Map[String, Money] = Map(
"James" > Money(500, 98),
"Jimmy" > Money(500, 44)
)
val marbles: Map[String, Int] = Map(
"James" > 4,
"Jimmy" > 5
)
val won: Map[String, Int] = Map(
"James" > 2,
"Jimmy" > 1
)
}

view raw
Semigroups1.scala
hosted with ❤ by GitHub

Step1:

We have class Money which contains states called dollars and cents. We know, the behavior of money is that, it can either be added or subtracted. For that, we need to create a method called add .

//add money function
def addMoney(money1: Money, money2: Money): Money = {
Money(money1.dollars + money2.dollars + ((money1.cents + money2.cents) / 100),
(money1.cents + money2.cents) % 100)
}

view raw
Semigroups2.scala
hosted with ❤ by GitHub

Step2:

Now we have add method for money, but what if we require to add Ints, Floats or even Maps? For this, we need to create separate methods for each:

// add any two integer value
def addInt(a: Int, b: Int): Int = a + b
// add employe salary to their account balances
def addMoneyMap(balances: Map[String, Money], salary: Map[String, Money]): Map[String, Money] = {
balances.foldLeft(salaries){
case (acc, (name, money)) =>
acc + (name > acc.get(name).map(addMoney(_ , money)).getOrElse(money))
}
}

view raw
Semigroups3.scala
hosted with ❤ by GitHub

Step3:

Now, we have three methods, addMoney, addInt, and addMap. But all of the methods perform the same binary operation. So, if we require a generic method for all, How can  we do that?

Scala provides a beautiful feature called traits. We need to create one trait and create an abstract method called add in the trait.

// generic trait addable
trait Addable[T] {
def add(a: T, b: T): T
}

view raw
Semigroups4.scala
hosted with ❤ by GitHub

Step4:

Now, we have a common trait which contains an abstract method called add and we are pretty much familiar, according to our requirement we can implement add method based on our required types like Int, Float, Money, Map and more.

So, before moving into the type implementations for the add method, we will be creating another method, which creates an abstraction between implementation and performs an operation according to passed types as below:

// method abstraction basis on type
def add[A: Addable](a: A, b: A)(implicit addable: Addable[A]): A = addable.add(a, b)

view raw
Semigroups5.scala
hosted with ❤ by GitHub

In this snippet, we are doing nothing, just using only another beautiful feature of Scala called implicit and execute the method add in Addable type.

Step5:

Now, let’s create an implementation of Addable type add method according to our requirements. Currently, we require to implement add method, for Int, Money and Map types. So we code as below:

// add implementation for int type
implicit val addInt = new Addable[Int] {
override def add(a: Int, b: Int): Int = a + b
}
// add implementation for money type
implicit val addMoney = new Addable[Money] {
override def add(a: Money, b: Money): Money = {
Money(a.dollars + b.dollars + ((a.cents + b.cents) / 100),
(a.cents + b.cents) % 100)
}
}
// add implementation for add two maps
// [V: Addable] is shothand of (implicit addable: Addable[A])
implicit def addMap[K, V: Addable] = new Addable[Map[K, V]] {
override def add(a: Map[K, V], b: Map[K, V]): Map[K, V] = {
a.foldLeft(b) {
case (acc, (x, y)) =>
acc + (x > acc.get(x).map(implicitly[Addable[V]].add(_, y)).getOrElse(y))
}
}
}

view raw
Semigroups6.scala
hosted with ❤ by GitHub

Step6:

Now, if we call the add method, which interacts with the user and create an abstract layer, so according to passed type the method executes the implementation and gives us a result. The whole code of the example is as below:

object Example1Semigroup extends App with Data {
trait Addable[T] {
def add(a: T, b: T): T
}
implicit val addInt = new Addable[Int] {
override def add(a: Int, b: Int): Int = a + b
}
implicit val addMoney = new Addable[Money] {
override def add(a: Money, b: Money): Money = {
Money(a.dollars + b.dollars + ((a.cents + b.cents) / 100),
(a.cents + b.cents) % 100)
}
}
// [V: Addable] is shothand of (implicit addable: Addable[A])
implicit def addMap[K, V: Addable] = new Addable[Map[K, V]] {
override def add(a: Map[K, V], b: Map[K, V]): Map[K, V] = {
a.foldLeft(b) {
case (acc, (x, y)) =>
acc + (x > acc.get(x).map(implicitly[Addable[V]].add(_, y)).getOrElse(y))
}
}
}
def add[A: Addable](a: A, b: A)(implicit addable: Addable[A]): A = addable.add(a, b)
println(s"Salary credit in you account xxxxxxx ${add(balance, salary)}")
println(s"Salary transfer to all employees ${add(balances, salaries)}")
println(s"Your game marbles balance is: ${add(marbles, won)}")
}
// output
Salary credit in you account xxxxxxx Money(422,44)
Salary transfer to all employees Map(James > Money(713,96), Jimmy > Money(543,88))
Your game marbles balance is: Map(James > 6, Jimmy > 6)

view raw
Semigroups7.scala
hosted with ❤ by GitHub

But still, In the whole example where are semigroups???use semigroups

So, if you remember, one of the properties in semigroups is closure. Where we perform some operation on 2 elements of set and answer belongs to the same set. If we look into the Addable add method, which performs some operation on the basis of type and returns the same type, that exactly is called semigroup.

Semigroups in functional programming contain only one method called combine, which combines the same type elements and return the same type of results. In the above example, we need to write a lot of custom code but Scala has a beautiful library called scala-cats, which contains predefined interface called Semigroup and that interface contains a method called combine. So, we need to implement combine method according to our type but scala-cats provide a lot of predefined implementations of combine method according to predefined types like Int, Double, Map, List and more.

Now we need to refactor above example according to scala-cats  as below:

object Example4Semigroup extends App with Data {
implicit val moneySemigroup = new Semigroup[Money] {
override def combine(x: Money, y: Money): Money = {
Money(x.dollars + y.dollars + ((x.cents + y.cents) / 100),
(x.cents + y.cents) % 100)
}
}
import cats.instances.int._
import cats.instances.map._
def add[A: Semigroup](a: A, b: A)(implicit semigroup: Semigroup[A]): A = semigroup.combine(a, b)
println(s"Salary credit in you account xxxxxxx ${add(balance, salary)}")
println(s"Salary transfer to all employees ${add(balances, salaries)}")
// another way to call combine method with beautiful syntax using cats
import cats.syntax.semigroup._
println(s"Salary transfer to all employees ${balances |+| salaries}")
println(s"Your game marbles balance is: ${marbles |+| won}")
}
// output
// Salary credit in you account xxxxxxx Money(422,44)
// Salary transfer to all employees Map(James -> Money(713,96), Jimmy -> Money(543,88))
// Salary transfer to all employees Map(James -> Money(713,96), Jimmy -> Money(543,88))
// Your game marbles balance is: Map(James -> 6, Jimmy -> 6)

view raw
Semigroups8.scala
hosted with ❤ by GitHub

The whole examples are picked from this blog. They have explained semigroups is an easy manner with real-life examples and I really love it. Following are some real-life scenarios where semigroups come into the picture:

  1. Domain Modeling: While you design your model according to your business domain, you should identify, your domain has a property like a combine or not.
  2. Combine multiple logs parallelly: As we know, semigroups also support the associative property, that means, we can easily distribute our implementation between multiple clusters of gather logs and at last, combine them all.
  3. and more…

References:

 


knoldus-advt-sticker


Written by 

Harmeet Singh is a lead consultant, with experience of more than 5 years. He has expertise in Scala, Java, JVM, and functional programming. On a personal front; he is a food lover.

4 thoughts on “ScalaFP: Understanding Semigroups In Scala Pragmatically4 min read

Comments are closed.

%d bloggers like this: