Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

When exactly I have to use State Monad or .copy?

I saw an example in a book of haskell in which is created a simple stack (push, pop) and return monad states all the time they update the stack using .put(..) or .pop().

As a reminder the example looks similar to this:

pop :: State Stack Int  
pop = State $ \(x:xs) -> (x,xs)  

push :: Int -> State Stack ()  
push a = State $ \xs -> ((),a:xs) 

It makes sense, but after seeing this example I started to wonder about my domain entities (let's say Customer). For now i'm just using .copy() to return a new updated-Customer everytime i want to update something on it (like the age) like is pointed in many docs, not using any State monad. The way I use .copy() has nothing special:

case class Customer(age: Int, name: String) {
   def updateAge(newAge: Int): Customer = {
       this.copy(age = newAge)
   }
}

However I'm staring to think that rather than returning just an updated-copy i have to return an State Monad, but I didn't see any example like this so far (I'm learning on my own), and because of that I feel confuse. I have the impression that .copy() is used sometimes and State Monad is used in other different cases, however both manage State.....

Should I return an State Monad everytime I update the age of a Customer?

like image 824
Francisco Albert Avatar asked Dec 17 '22 15:12

Francisco Albert


1 Answers

You never have to return a State value—it just makes certain kinds of composition possible, which makes certain ways of building your program cleaner.

For topics like this I think starting from a concrete, clearly motivated example is best, and the Circe JSON library includes some State-based convenience methods for decoding that I think can help to show how State can be useful. For example, suppose we have a Scala case class:

case class User(id: Long, name: String, posts: List[Long])

We want to be able to decode JSON documents like this:

val goodDocument = """{"id": 12345, "name": "Foo McBar", "posts": []}"""

But not ones like this:

val badDocument =
  """{"id": 12345, "name": "Foo McBar", "posts": [], "bad-stuff": null}"""

If we write out a Circe decoder for User by hand, it will probably look something like this:

implicit val decodeUser: Decoder[User] = Decoder.instance { c =>
  for {
    id    <- c.downField("id").as[Long]
    name  <- c.downField("name").as[String]
    posts <- c.downField("posts").as[List[Long]]
  } yield User(id, name, posts)
}

Unfortunately this doesn't meet our requirements—it accepts both documents, because it doesn't keep track of what fields have been decoded already and what fields are left over.

scala> decode[User](goodDocument)
res0: Either[io.circe.Error,User] = Right(User(12345,Foo McBar,List()))

scala> decode[User](badDocument)
res1: Either[io.circe.Error,User] = Right(User(12345,Foo McBar,List()))

We can fix this by keeping track ourselves and "deleting" fields after we see them:

import io.circe.{DecodingFailure, Json}

implicit val decodeUser: Decoder[User] = Decoder.instance { c =>
  val idC = c.downField("id")

  def isEmptyObject(json: Json): Boolean = json.asObject.exists(_.isEmpty)

  for {
    id          <- idC.as[Long]
    afterIdC     = idC.delete
    nameC        = afterIdC.downField("name")
    name        <- nameC.as[String]
    afterNameC   = nameC.delete
    postsC       = afterNameC.downField("posts")
    posts       <- postsC.as[List[Long]]
    afterPostsC  = postsC.delete
    _           <- if (afterPostsC.focus.exists(isEmptyObject)) Right(()) else {
      Left(DecodingFailure("Bad fields", c.history))
    }
  } yield User(id, name, posts)
}

Which works great!

scala> decode[User](goodDocument)
res2: Either[io.circe.Error,User] = Right(User(12345,Foo McBar,List()))

scala> decode[User](badDocument)
res3: Either[io.circe.Error,User] = Left(DecodingFailure(Bad fields, List()))

But the code is a mess, since we have to interleave our logic for keeping track of what's been used through our nice clean Decoder.Result-based for-comprehension.

The way Circe supports decoders that keep track of what fields have been used is based on Cats's StateT. Using these helper methods, you can write the following:

import cats.instances.either._

implicit val decodeUser: Decoder[User] = Decoder.fromState(
  for {
    id    <- Decoder.state.decodeField[Long]("id")
    name  <- Decoder.state.decodeField[String]("name")
    posts <- Decoder.state.decodeField[List[Long]]("posts")
    _     <- Decoder.state.requireEmpty
  } yield User(id, name, posts)
)

This looks a lot like our original (non-used-field-tracking) implementation, but it works exactly the same way as our second implementation.

We can look at the type of a single operation:

scala> Decoder.state.decodeField[String]("name")
res4: StateT[Decoder.Result,ACursor,String] = ...

Which is a kind of wrapper for the following:

ACursor => Decoder.Result[(ACursor, Long)]

This is basically combining the afterIdC.downField("name"), nameC.delete, and nameC.as[String] lines from our verbose implementation above, but encapsulating them in a single operation that we can use in a for-comprehension.

Calling copy on a case class to get an updated value is of course perfectly fine, and most of the time it's exactly what you want. You shouldn't complicate your code with State unless you're sure it's adding value, and it's most likely to add value when you find yourself threading calls to copy and assignments through some other kind of operation, like we saw with field-use tracking and decoding above.

like image 183
Travis Brown Avatar answered Jan 07 '23 02:01

Travis Brown