4

Grokking Monad in Scala

 3 years ago
source link: https://blog.oyanglul.us/grokking-monad/scala/en/part2
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Type classes

Type classes is somewhat like an FP design pattern, with type classes you can extend new functionality without touch any of your existing code or 3rd party library. Type classes vs OO classes

Add new method Add new data OO Change existing code Existing code unchanged FP Existing code unchanged Change existing code

But FP has pretty elegant [way](https://oleksandrmanzyuk.wordpress.com/2014/06/18/from-object-algebras-to-finally-tagless-interpreters-2/) or [ways](http://www.cs.ru.nl/~W.Swierstra/Publications/DataTypesALaCarte.pdf) to walk around this issue.

Algebraic Data Type

We all familiar with how to implement traffic light in OO with class and interface. Simply 3 classes Red Green Yellow extend a TrafficLight interface, and implement next method in each of the 3 classes.

But how can we do differently with Scala trait and pattern matching?

behavior of "TrafficLight"

it should "follow the rules" in {
  Red.next shouldBe Green
  Green.next shouldBe Yellow
  Yellow.next shouldBe Red
}

The type of TrafficLight you've just implemented is call Algebraic Data Type(ADT), which means the type is algebra of Red + Green + Yellow, type of algebra of + is called Sum or Coproduct type. The other algebra is x, we've already tried case class in Scala, which is a Product Type. e.g. Cat is product type because it's String x String. You also can simply translate + as or and x as and then it will make more sense.

Recursive Data Type

with ADT it's very easy to implement a linked list in Scala as well. Try using sum and product type to implement a LinkedList. Type can be recursive as well.

behavior of "LinkedList of 2 elements"

it should "be able to get first" in {
  Pair(1, Pair(2, End()))(0) shouldBe 1
}

it should "be able to get second element" in {
  Pair(1, Pair(2, End()))(1) shouldBe 2
}

it should "throw error Out of Boundary exception if try to get 3rd" in {
  the[Exception] thrownBy Pair(1, Pair(2, End()))(2) should have message "Out of Boundary"
}

Variance

You may realize that End does not have to be a case class, it doesn't have any value in it, and we just need one instance of End.

Try changing it into case object, uncomment the following case and fix the compiler errors.

package typeclass

import org.scalatest._

class `2.3.Variance` extends FlatSpec with Matchers {

behavior of "Covarian LinkedList"

it should "have the same behaviors as LinkedList" in {
  pending
  // CoPair(1, CoPair(2, CoEnd))(0) shouldBe 1
  // CoPair(1, CoPair(2, CoEnd))(1) shouldBe 2
  // the [Exception] thrownBy CoPair(1, CoPair(2, CoEnd))(2) should have message "Out of Boundary"
}

The way you fix the compiler error is called Covarian, which means if B is A's subtype, CoLinkedList[B] is then CoLinkedList[A]'s subtype

Here Nothing is subtype of any type, since A is covariance, End of type CoLinkedList[Nothing] become subtype of CoLinkedList[A]

Type classes

Person is a simple case class, when we want to print it nicely as JSON format, what we usually does is to create a interface e.g. JsonWriter with method toJson and implements it in Person. But if you think about it, if we need Person to have another ability e.g. HtmlWriter, you need to open Person class and implement a new interface.

However, FP does it completely different, with type classes, we can leave Person completely untouched and define it's behavior in type class, and then implicitly implement the type class for Person anywhere.

Now try not touching class Person and let it able to print as JSON and sortable by name.

behavior of "Person"

it should "able to convert to JSON" in {
  JsonWriter.write(Person("o", "[email protected]")) shouldBe """{"name": "o", "email": "[email protected]"}"""
}

it should "able to sort by name" in {
  List(Person("a", "d"), Person("b", "c")).sorted shouldEqual List(
    Person("a", "d"),
    Person("b", "c"))
  List(Person("b", "c"), Person("a", "d")).sorted shouldEqual List(
    Person("a", "d"),
    Person("b", "c"))
}

It's easy to add the same behavior to any other type as well.

behavior of "Cat"
it should "able to convert to JSON" in {
  JsonWriter.write(Cat("Garfield", "coke")) shouldBe """{"name": "Garfield", "food": "coke"}"""
}

behavior of "CatPerson"

it should "be very easy to convert to JSON" in {
  JsonWriter.write(CatPerson(Person("a", "b"), Cat("Garfield", "chips"))) shouldBe """{"person":{"name": "a", "email": "b"},"cat":{"name": "Garfield", "food": "chips"}}"""
}

Type enrichment

With implicit class, you can magically add methods to any Type

For example to add a new method numberOfVowels to String type, we can simply define a implicit class, and add the method there

implicit class ExtraStringMethods(str: String) {
  val vowels = Seq('a', 'e', 'i', 'o', 'u')

  def numberOfVowels =
    str.toList.filter(vowels contains _).length
}

When you do "the quick brown fox".numberOfVowels, Scala compiler can't find numberOfVowels in String type, but it will try to find an implicit class which has a numberOfVowels, if it can find one. Here compiler found ExtraStringMethods, then it will implicitly create an instance of ExtraStringMethods from string "the quick brown fox", so calling numberOfVowels will just work like it's builtin implemented method of String type.

it should "able to use `writeJson` method" in {
  CatPerson(
    Person("oyjc", "[email protected]"),
    Cat("Hello Kitty", "rainbow")).writeJson shouldBe """{"person":{"name": "oyjc", "email": "[email protected]"},"cat":{"name": "Hello Kitty", "food": "rainbow"}}"""
}

But, it's not generic enough, we still need to implement Cat.writeJson and Person.writeJson. How can we have a generic writeJson method which automatically works for all JsonWrite[_] type

"Cat and Person" should "also be able to use `writeJson` without any changes" in {
  import JsonWriter.Ops
  Cat("Garfield", "chips").writeJson shouldBe """{"name": "Garfield", "food": "chips"}"""
  Person("Philip", "Fry").writeJson shouldBe """{"name": "Philip", "email": "Fry"}"""
}

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK