Reader Monad in cats

After studying Monad, I also curious about Reader Monad and Writer Monad. So, I write a post about it. In fact, first, I suppose to write a Reader Monad and Writer Monad at the same post, but later I decided to write separately.

Below example is from mastering advanced Scala

What is Reader Monad

At first, I thought Reader Monad is used to read something. It’s true. But another definition is effective.

Reader Monad is used to Dependency Injection.

Yes. I think it’s a better definition. Reader Monad object is like this.

object Reader {
  def apply[A, B](f: A => B): Reader[A, B] = ReaderT[Id, A, B](f)
}

Anonymous function f is the function we want to run. And ‘A’ will be injected later when we call like this.


readerMoandValue.run(ATypeInstance)

ATypeInstance is an instance which type is A. And as you can see, is passed when you call run function.

Example

First, Let’s suppose some Service like this.

trait AuthService {
  def isLogged(name: String): Boolean
}

class AuthServiceChar3 extends AuthService{
  override def isLogged(name: String): Boolean = name.length == 3
}

class AuthServiceChar5 extends AuthService{
  override def isLogged(name: String): Boolean = name.length == 5
}

trait UserService {
  def greet(name: String, isLogged: Boolean): String
}

class UserServiceDefaultUser extends UserService{
  override def greet(name: String, isLogged: Boolean): String = {
    val actualName = if(isLogged) name else "User"

    s"Hello $actualName"
  }
}

class UserServiceNoDefault extends UserService{
  override def greet(name: String, isLogged: Boolean): String = {
    if(isLogged) s"Hello $name" else "No authorization"
  }
}

case class Environment(userName: String, userService: UserService, authService: AuthService)

AuthService has a simple auth logic and actualized class is AuthServiceChar3 and AuthServiceChar5. And UserSertivce has a simple greet logic and actualized class is UserServiceDefaultUser and UserServiceNoDefault.

And Environment is case class which needs a user name, UserService, and AuthService. And here are some Reader Monad use environment and return something.

import cats.data.Reader

def isLoggedUser: Reader[Environment, Boolean] = Reader[Environment, Boolean] { env =>
  env.authService.isLogged(env.userName)
}

def greetUser(logged: Boolean): Reader[Environment, String] = Reader[Environment, String] { env =>
  env.userService.greet(env.userName, logged)
}
// In Intellij if you use auto complete, Kleisli[Id, Environment, String] will be written. I will write post later
val resultR: Reader[Environment, String] = for {
  logged <- isLoggedUser
  greeting <- greetUser(logged)
} yield greeting

Value resultR is a Reader Monad get logged value from isLoggedUser and return greet String.

In here yet, we didn’t pass Environment instance. Now, let’s pass Environment.


val environment1 = Environment("Joe", new UserServiceDefaultUser, new AuthServiceChar3)

println(resultR.run(environment1))  // print Hello Joe

val environment2 = Environment("Joe", new UserServiceNoDefault, new AuthServiceChar5)

println(resultR.run(environment2))  // print No authorization

As you can see, we inject environment when we call run function. And when we inject different instance, print different output.

trait AuthService {
def isLogged(name: String): Boolean
}
class AuthServiceChar3 extends AuthService{
override def isLogged(name: String): Boolean = name.length == 3
}
class AuthServiceChar5 extends AuthService{
override def isLogged(name: String): Boolean = name.length == 5
}
trait UserService {
def greet(name: String, isLogged: Boolean): String
}
class UserServiceDefaultUser extends UserService{
override def greet(name: String, isLogged: Boolean): String = {
val actualName = if(isLogged) name else "User"
s"Hello $actualName"
}
}
class UserServiceNoDefault extends UserService{
override def greet(name: String, isLogged: Boolean): String = {
if(isLogged) s"Hello $name" else "No authorization"
}
}
case class Environment(userName: String, userService: UserService, authService: AuthService)
import cats.data.Reader
def isLoggedUser: Reader[Environment, Boolean] = Reader[Environment, Boolean] { env =>
env.authService.isLogged(env.userName)
}
def greetUser(logged: Boolean): Reader[Environment, String] = Reader[Environment, String] { env =>
env.userService.greet(env.userName, logged)
}
// In Intellij if you use auto complete, Kleisli[Id, Environment, String] will be written. I will write post later
val resultR: Reader[Environment, String] = for {
logged <- isLoggedUser
greeting <- greetUser(logged)
} yield greeting
val environment1 = Environment("Joe", new UserServiceDefaultUser, new AuthServiceChar3)
println(resultR.run(environment1))
val environment2 = Environment("Joe", new UserServiceNoDefault, new AuthServiceChar5)
println(resultR.run(environment2))
Advertisement

Pattern Matching & unapply & apply

In Scala, we use pattern matching very often. It’s made by unapply. But because we use this by making case class, I always forget how to make it. I write this post to remind me. All example uploaded in below(gist).

How to make apply & unapply

Normal Pattern Matching

Let’s say that there is a class defined like this.


class Person(val name: String, val age: Int, val weight: Int)

And we can write apply by making companion object.


object Person {
  def apply(name: String, age: Int, weight: Int): Person = new Person(name, age, weight)
}

We can also write unapply in companion object like this.


object Person {
  def apply(name: String, age: Int, weight: Int): Person = new Person(name, age, weight)

  def unapply(arg: Person): Option[(String, Int, Int)] =
    if(arg.age > 20) Some(arg.name, arg.age, arg.weight)
    else None
}

Unapply return Option Tuple. After the operation, if it returns Some, it matches to case class. If it returns None, it does not match. We can use it like this.

val young: Person = Person("Martin Junior", 14, 50)
val old: Person = Person("Martin", 29, 70)

def PatternMatching(person: Person): Unit = person match {
  case Person(name, age, weight) => println(s"name: $name, age: $age, weight: $weight")
  case _ => println(s"${person.name} is under 20")
}

In apply, If age is under 20 it returns None so, young is not matched. And old is matched. Yes! it’s simple! But I have additional Curious.

How about pattern matching partial?

What if I don’t want to expose weight in pattern matching? It’s simple!

class SecretPerson(val name: String, val age: Int, val weight: Int)

object SecretPerson {
  def apply(name: String, age: Int, weight: Int): SecretPerson = new SecretPerson(name, age, weight)

  def unapply(arg: SecretPerson): Option[(String, Int)] =
  if(arg.age >= 20) Some(arg.name, arg.age)
  else None
}

def PatternMatchingSecret(secretPerson: SecretPerson): Unit = secretPerson match {
  case SecretPerson(name, age) => println(s"name: $name, age: $age")
  case _ => println(s"${secretPerson.name} is under 20")
}

val secretYoung: SecretPerson = SecretPerson("Martin Junior", 14, 50)
val secretOld: SecretPerson = SecretPerson("Martin", 29, 70)

PatternMatchingSecret(secretYoung)
PatternMatchingSecret(secretOld)

Just return Option Tuple without weight. It is possible!

Case class unapply overriding

How about case class? It already has pre-defined unapply. What if I also want to hide weight this time? Let’s do it!


case class CasePerson(name: String, age: Int, weight: Int)
object CasePerson {
//  Compile Error - unapply is defined twice
//  def unapply(arg: CasePerson): Option[(String, Int)] =
//    if(arg.age >= 20) Some(arg.name, arg.age)
//    else None
}

val caseYoung: CasePerson = CasePerson("Martin Junior", 14, 50)
val caseOld: CasePerson = CasePerson("Martin", 29, 70)

def PatternMatchingCase(casePerson: CasePerson): Unit = casePerson match {
//   case CasePerson(name, age) => println(s"name: $name, age: $age") compile error: wrong number of arguments
  case CasePerson(name, age, weight) => println(s"name: $name, age: $age, weight: $weight")
  case _ => println(s"${casePerson.name} is under 20")
}

It’s impossible. We can get compile error in override ‘unapply is defined twice’. And also in case matching part ‘wrong number of arguments’.

class Person(val name: String, val age: Int, val weight: Int)
object Person {
def apply(name: String, age: Int, weight: Int): Person = new Person(name, age, weight)
def unapply(arg: Person): Option[(String, Int, Int)] =
if(arg.age >= 20) Some(arg.name, arg.age, arg.weight)
else None
}
val young: Person = Person("Martin Junior", 14, 50)
val old: Person = Person("Martin", 29, 70)
def PatternMatching(person: Person): Unit = person match {
case Person(name, age, weight) => println(s"name: $name, age: $age, weight: $weight")
case _ => println(s"${person.name} is under 20")
}
PatternMatching(young)
PatternMatching(old)
class SecretPerson(val name: String, val age: Int, val weight: Int)
object SecretPerson {
def apply(name: String, age: Int, weight: Int): SecretPerson = new SecretPerson(name, age, weight)
def unapply(arg: SecretPerson): Option[(String, Int)] =
if(arg.age >= 20) Some(arg.name, arg.age)
else None
}
def PatternMatchingSecret(secretPerson: SecretPerson): Unit = secretPerson match {
case SecretPerson(name, age) => println(s"name: $name, age: $age")
case _ => println(s"${secretPerson.name} is under 20")
}
val secretYoung: SecretPerson = SecretPerson("Martin Junior", 14, 50)
val secretOld: SecretPerson = SecretPerson("Martin", 29, 70)
PatternMatchingSecret(secretYoung)
PatternMatchingSecret(secretOld)
case class CasePerson(name: String, age: Int, weight: Int)
object CasePerson {
// Compile Error - unapply is defined twice
// def unapply(arg: CasePerson): Option[(String, Int)] =
// if(arg.age >= 20) Some(arg.name, arg.age)
// else None
}
val caseYoung: CasePerson = CasePerson("Martin Junior", 14, 50)
val caseOld: CasePerson = CasePerson("Martin", 29, 70)
def PatternMatchingCase(casePerson: CasePerson): Unit = casePerson match {
// case CasePerson(name, age) => println(s"name: $name, age: $age") compile error: wrong number of arguments
case CasePerson(name, age, weight) => println(s"name: $name, age: $age, weight: $weight")
case _ => println(s"${casePerson.name} is under 20")
}

Covariant and Contravariant in Scala

Scala often uses type parameter and also, you can see [+A] or [-A] in many library codes. I also wonder what it is. So, I study this and discuss with my co-worker. And leave this post to understand me. (But also, any comments are always welcome!)

Special Thanks for Karellen in Kakao.

What is Covariant, Contravariant and Invariant?

In Scala School Co, Contra, In -variant is described like this.

Variance is  about “if A <: B ( A is a subtype of B), how is the relation between M[A] and M[B]?”

if A <: B… then,

Meaning Scala notation
covariant M[A] <: M[B] [+B]
contravariant M[B] <: M[A] [-B]
invariant M[B] and M[A] are not related [B]

This means for example, in Java

package org.ktz.example.blog;
import java.util.ArrayList;
import java.util.List;
public class Variance {
public static void main(String[] args) {
String str = "";
Object obj = str;
List<String> listStr = new ArrayList<>();
// List<Object> objects = listStr; // compile error
}
}
view raw Variance.java hosted with ❤ by GitHub

As you can see above,

List<Object> objects = listStr

cannot be compiled.

Because in Java List<Object> is not considered as a parent of List<String>.

But, in Scala,

val obj: Object = "Hello World!"
val listString: List[String] = List.empty
val listAny: List[Any] = listString
view raw Variance.scala hosted with ❤ by GitHub

It can be compiled. Because in Scala, List[Any] is considered as a parent of  List[String]. In Scala source code, List is set as covariant.

type List[+A] = scala.collection.immutable.List[A]
view raw ScalaList.scala hosted with ❤ by GitHub

Liskov Substitution Principle

It is very natural and simple. If A <: B, we can assign Typ A variable to Type B variable.


val a: A = new A

val b: B = a

also, List[+A] can be adapted, becase List is covariant.


val a: List[A] = List[A].empty

val b: List[B] = a

Variance in class

Guess, that I want to make my own List, and class like this.


class MyList[+A] {
    def insert(element: A): A = ??? // compile error
}

compile error message is


Error: covariant type A occurs in contravariant position in type A of value element
def insert(element: A): A = ???

Why does that happen? Simply thinking, I make a List of type A and subtype of A can be considered as a subtype of MyList[A]. Below topic explain of this.

Rethinking of + and –

In Scala, ‘+’ means Covariant and ‘-‘ means Contravariant. Let`s forget about it. And redefine ‘+’ as ‘Can receive subtype’ and ‘-‘ as ‘Can receive Supertype’. And then, above error can be described.

As defined in Liskov principle, In element, we can assign super type of A. But, +A is covariant. So, element can be only assigned subtype. So, this is contradiction.

So, if we correct the code like this, It can be compiled.


class MyList[+A] {
    def insert[B >: A](element: B): A = ???
}

By the way, how about return type as A. Is this ok? Of course yes. Let`s say that C <: A

And after long, long operation, the return type is decided as C. And as Liskov law, type C can be assigned to type A.(val c: C = A)

Variance in Function

In conclusion(as you can see above), Parameter is Contravariant(-A) and return type is Covariant. Also, in Scala, Function signature is


trait Function1[-T1, +R]

Yes, it`s simplified.

The subtype of Functions

In Scala course in coursera, there is a interesting problem. (It`s motivation of this post)

There are types like this.


NoneEmpty <: IntSet

type A = IntSet => NoneEmpty

type B = NoneEmpty => IntSet

What is the relationship between A and B?

  • A <: B
  • A >: B

The answer is A <: B. Why? As Liskov Principle, A can be assigned as B. It does not feel the impact to me. So, Let`s say that

if A <: B, A can pretend as B

Yes, String can pretend as Object. And above, NoneEmpty can pretend as IntSet.

Ok, then type A (IntSet => NoneEmpty) can pretend as type B(NoneEmpty => IntSet)? Of course!

If User passes NoneEmpty to A, It can be assigned to IntSet. And after an operation, A return NoneEmpty. And NoneEmpty can be assigned to IntSet.

subtypeFunction.png

A can pretend as B and A <: B