framework
framework copied to clipboard
Explore additional Box combinators and hierarchical improvements
A few possibilities:
- Add
collecttoBox. This can be useful forPartialFunction-based combined filter+map operations on boxes as they are on collections. - Add some sort of
mapFailurethat behaves similarly tomapbut only runs with aFailure. This would allow running a function only when the box is aFailure, without having to do amatchthat has to deal with the other three box states. - Restructure the
Boxhierarchy a little to allow a type that indicates “onlyEmptyorFull” and another that indicates “onlyFailureorFull”, for example. - Add logging helpers for
Boxif possible (may need to be inlift-util).
For the last one, an untested proposal I posted to gitter a while back:
sealed trait Box[+T]
sealed trait PresenceBox[+T] extends Box[T]
sealed trait TryBox[+T] extends Box[T]
sealed abstract class EmptyBox extends Box[Nothing] // Empty or Failure
case class Failure(msg: String, exception: PresenceBox[Throwable], chain: PresenceBox[Failure]) extends EmptyBox with TryBox[Nothing]
case object Empty extends EmptyBox with PresenceBox[Nothing]
case class Full[T](item: T) extends TryBox[T] with PresenceBox[T]
Can I take this up?
Sure, happy to see any attempts. My example above forgot to have Empty extend PresenceBox.
Alright.
Also, what kind of logging helpers are you looking for? Any approximate examples or a rough idea?
Oh, logging helper wise I actually just want to port a bunch of stuff we have at Elemica into Lift. So I've more or less already figured this out, if that's okay… This was definitely a brain dump for myself, sorry.
Alright. I'll leave that part to you then. :smiley:
Another thought on the hierarchy:
sealed trait ParamFailableBox[+T, A] extends Box[T]
case class ParamFailure[A](..., param: A) extends ParamFailableBox[Nothing, A]
case class Full[T](item: T) extends TryBox[T] with PresenceBox[T] with ParamFailableBox[T, Nothing]
Not 100% sure that bit'll work, but it allows you to declare that something expects only a Full and a ParamFailure with a particular type to be produced.
Looks good. That will be helpful.
One thing I wanted to make sure was what you are expecting from mapFailure. For ex. here's what I thought: using mapFailure will be an optional stage in the map/flatMap/filter pipeline, which will only get applied if the Box was a Failure. Otherwise, that stage will simply be skipped and the maps/flatMaps down the line will still be applied.
The final result will still be a Box. If the Box is indeed a failure, the operation will be applied and any further map/flatMap etc. will be skipped.
In other words: mapFailure is a way to transform your failure if it happens, otherwise go ahead with the rest of the operations as if there was no mapFailure call in the middle.
I haven't thought this through completely yet. But, is that how you saw it?
Yes, I think that was my overall thinking. Since I jotted down some older ideas floating in my head I'm not certain that's the case though… A sample use case might be to transform between one set of ParamFailures that a service A produces to the set that service B produces, so that a consumer of service B only needs to know about its error codes.
Alright. I think I'll create a small WIP PR soon. Maybe you can comment on relevant parts, whether or not it's what you expected and I'll make the necessary changes. Hopefully that won't be too much trouble for you. But do let me know if that doesn't work for you.
Heh, figured out another one here, which is flipping an EmptyBox to a Full. This is useful for error handling, where sometimes you want something that will take an errored box and turn it into a Full of a different kind (e.g., a CSS selector transform). One could imagine:
def myRenderer = {
".error*" #> user.logFailure("Data fetch went sideways").flip({
case Empty => "Failed to find data."
case ParamFailure(_, _, _, userError: String) => s"Error: $userError"
case Failure(internalMessage, _, _) => s"Internal error: $internalMessage"
}) &
".name *" #> user.map(_.name) &
".age *" #> user.map(_.age)
}
Don't know that flip is the right term of course ;) This could look like, for EmptyBox:
def flip[T](handler: (EmptyBox)=>T): PresenceBox[T] = {
Full(handler(this))
}
And then Full would always return Empty. Thoughts? (I used PresenceBox from the initial bit of this issue…)
Looks good. I can see it being useful. flip sounds like a good name, since it turns empty to full and full to empty. Any further combinators after flip would only work if the initial result was an EmptyBox. If the user needed to do something for user.logFailure("Data fetch went sideways") being full, it would have to be done separately, and that does seem appropriate for this kind of a use case.
Does a similar flipFailure method sound useful? We are already adding a collectFailure method as well. We could end up with too may combinators though.
Also, I think we will get conflicts when there's a need to merge my branch with your box-hierarchy branch. I'll try to push some final changes and then it can be merged into yours if things look appropriate maybe? Or if you want, I can do the merge and modify things as needed with the new hierarchy. Adding tests and so on.
No, I think flip handles flipFailure just fine---it takes an EmptyBox, but if you want to pattern match inside it you can. I guess it won't let you only flip a Failure… I would say it's worth waiting to see if that's a material use-case in practice though.