1
votes

I'm working on a game prototype and trying to be as pure as possible. All use-cases are fit into one scenario -

  1. Try to find player in a storage
  2. Perform some business logic
  3. Update player in the storage
  4. While updating one can produce some output - Log messages, Messages to another players, etc.

From another side one have to get access to Environment (databases, resources, etc), global game state (immutable game configs, seeds etc).

To tie it all together I ended up with scalaz7 ReaderWriterState monad like this:

Some definitions:

trait UserService
trait Environment
trait State
sealed trait Error
sealed trait Output

case object GameEnvironment extends Environment
case object GameState extends State
object Output {
  case object Log extends Output
  case object Parcel extends Output
  case object Analytics extends Output
}
object Error {
  case class AppError(code: String) extends Error
  case class ThrowableError(ex: Exception) extends Error
}

Service methods return type - provides access to Environment via Reader, produces some output via Writer, gives access to GameState and produces method result - Error or Some type

type Result[T] = ReaderWriterState[Environment, List[Output], State, Error \/ T]

Just an example on how Service might be implemented

object UserServiceImpl extends UserService {
  def findPlayer(id: Long): Result[Player] = ReaderWriterState { (env, state) =>
    (
      Nil, 
      \/-(Player(id, "name")), 
      state
    )
  }
  def updatePlayer(player: Player): Result[Player] = ReaderWriterState { (env, state) =>
    (
      List(Output.Log), 
      \/-(player.copy(name = "updated")), 
      state
    )
  }
}

Above mentioned scenario is (won't compile):

val (out, res, state) = (for {
  playerOrError <- userService.findPlayer(1L)         //How to short-circuit if findPlayer returns left either?
  updated <- userService.updatePlayer(playerOrError)  //How to transform playerOrError to right projection and pass it here?
} yield player).run(GameEnvironment, GameState)

So, my questions are:

  1. How to short-circuit if findPlayer:RWS returns left either?
  2. How to transform playerOrError to right projection and pass it here?

It looks like I can try to use transformer somehow but can't get my head around it.

Thanks!

2

2 Answers

1
votes

Use ReaderWriterStateT:

type Result[T] = ReaderWriterStateT[Either[Error, ?], Environment, List[Output], State, T]

which is equivalent to

(Environment, State) => Either[Error, (List[Output], T, State)]

which also means that in case of error, no output is written and no state is changed.

If you really want to keep the same structure of Result that you have, use

type Result[T] = EitherT[ReaderWriterState[Environment, List[Output], State, ?], Error, T]
0
votes

Thanks to @tomas, it works and exits in case of left type.

Here is the result code:

type ErrorOr[+T] = Error \/ T
type Result[T] = ReaderWriterStateT[ErrorOr, Environment, List[Output], State, T]

object UserServiceImpl extends UserService {

  def findPlayer(id: Long): Result[Player] = ReaderWriterStateT { (env, state) =>
    val player = Player(1L, "name")
    \/-((List.empty[Output], player, state))
  }

  def updatePlayer(player: Player): Result[Player] = ReaderWriterStateT { (env, state) =>
    \/-((List.empty[Output], player.copy(name = "updated"), state))
  }

}

val userService = UserServiceImpl

val result = (for {
  player <- userService.findPlayer(1L)
  updated <- userService.updatePlayer(player)
} yield updated).run(GameEnvironment, GameState)

result match {
  case \/-((out, player, state)) => println(player)
  case -\/(error) => println(error)
}