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?
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With