Here is a code example:
import cats.data.Reader
trait Configuration {
type FailFast[A] = Either[List[String], A]
def getValue(name: String)(map: Map[String, String]): FailFast[String] =
map.get(name)
.toRight(List(s"$name field not specified"))
type PropReader[A] = Reader[Map[String, String], A]
def propReader(name:String): PropReader[FailFast[String]] =
Reader(map => validation.getValue(name)(map))
type OptionalValue[A] = PropReader[FailFast[Option[A]]]
//how to use propReader(Configuration.NEW_EVENT)
//inside of 'event' to return 'OptionalValue':?
def event:OptionalValue[String] = ???
}
object Configuration extends Configuration {
final val NEW_EVENT = "event.unique"
}
Cannot get it how to implement event with a composition of: propReader(Configuration.NEW_EVENT)
If there are more than 1 options, it would be great to consider all of them.
UPDATE thanks to @Travis Brown, I would implement it this way. Here is an updated implementation:
import cats.instances.list._ //for monoid
import cats.instances.either._
type FailFast[A] = Either[List[String], A]
type PropReaderT[A] = ReaderT[FailFast, Map[String, String], A]
type OptionalReaderT[A] = ReaderT[FailFast, Map[String, String], Option[A]]
def getValue(name: String)(map: Map[String, String]): FailFast[String] =
map.get(name).toRight(List(s"$name field not specified"))
def propReader(name: String): PropReaderT[String] =
ReaderT(getValue(name))
def value2Option(value:String):Option[String] =
if (value == null || value.isEmpty) Option.empty
else Some(value)
def event: OptionalReaderT[String] =
propReader(Configuration.KEY1)
.map(result => value2Option(result))
The difference between this and Travis Brown's implementation: I need to see a difference between not having a key in the map (which is an error, and I need a clear error description of it) and a case when a key exists, but its value either null or empty string. So it does not work in the same way as Maps.get, which returns Option. So I cannot get rid of FailFast
Hope for someone, it will be useful.
The simplest approach would be to map into the result, promoting failures into a successful None
:
import cats.data.Reader
trait Configuration {
type FailFast[A] = Either[List[String], A]
type PropReader[A] = Reader[Map[String, String], A]
type OptionalValue[A] = PropReader[FailFast[Option[A]]]
def getValue(name: String)(map: Map[String, String]): FailFast[String] =
map.get(name).toRight(List(s"$name field not specified"))
def propReader(name:String): PropReader[FailFast[String]] =
Reader(getValue(name))
def event: OptionalValue[String] = propReader(Configuration.NEW_EVENT).map(
result => Right(result.right.toOption)
)
}
object Configuration extends Configuration {
final val NEW_EVENT = "event.unique"
}
I think it's worth reconsidering the model a bit, though. Any time you have a function that looks like A => F[B]
(like a lookup in a map), you can represent it as a ReaderT[F, A, B]
, which gives you nicer kinds of composition—instead of mapping through two layers, you only have one, for example.
The ReaderT
approach also makes it a little nicer to change out the F
(via mapK
). For example, suppose as in your example you generally want to work with readers that return their values in a FailFast
context, but you need to switch to an Option
context occasionally. That would look like this:
import cats.~>
import cats.arrow.FunctionK
import cats.data.ReaderT
trait Configuration {
type FailFast[A] = Either[List[String], A]
type PropReader[A] = ReaderT[FailFast, Map[String, String], A]
type OptionalReader[A] = ReaderT[Option, Map[String, String], A]
private def eitherToOption[A](either: FailFast[A]): Option[A] =
either.right.toOption
def getValue(name: String)(map: Map[String, String]): FailFast[String] =
map.get(name).toRight(List(s"$name field not specified"))
def propReader(name: String): PropReader[String] =
ReaderT(getValue(name))
def event: OptionalReader[String] =
propReader(Configuration.NEW_EVENT).mapK(FunctionK.lift(eitherToOption))
}
object Configuration extends Configuration {
final val NEW_EVENT = "event.unique"
}
The OptionalReader
here isn't exactly the same as your OptionalValue
, since it doesn't include the FailFast
layer, but that layer is redundant in your code, since missing values are represented in the Option
layer, so the OptionReader
approach is likely to be a better fit.