3

This is a follow-up to my previous question

Suppose I have two validating functions that return either the input if it is valid or the error messages if it is not.

type Status[A] = ValidationNel[String, A]

val isPositive: Int => Status[Int] = 
  x => if (x > 0) x.success else s"$x not positive".failureNel

val isEven: Int => Status[Int] = 
  x => if (x % 2 == 0) x.success else s"$x not even".failureNel

Suppose also that I need to validate an instance of case class X:

case class X(x1: Int, // should be positive 
             x2: Int) // should be even

More specifically I need a function checkX: X => Status[X]. Moreover, I'd like to write checkX as a composition of isPositive and isEven.

val checkX: X => Status[X] =
  ({x => isPositive(x.x1)} |@| {x => isEven(x.x2)}) ((X.apply _).lift[Status])

Does it make sense ?
How would you write checkX as a composition of isPositive and isEven?

Community
  • 1
  • 1
Michael
  • 41,026
  • 70
  • 193
  • 341
  • I would start learning about monads, now, because you have a (textbook example of a) monad at hand. – n. m. could be an AI Jun 01 '15 at 08:51
  • 1
    I know a little bit about monads. I do not think I need monads here. What monad exactly do you mean ? – Michael Jun 01 '15 at 08:53
  • `Status` is a monad and is composed as a monad, whether you think you need one or not. You are welcome to use it as a monad, or reinvent the wheel, your choice. – n. m. could be an AI Jun 01 '15 at 09:03
  • 1
    `Status` (`Validation`) is an applicative functor but not a monad. – Michael Jun 01 '15 at 09:05
  • Status is most certainly _not_ a monad as a monad instance does not exist for `Validation`. You are composing them using the applicative builder syntax, so I'm not sure how else you are wanting to use composition? I would definitely add types to express your intention better: `case class X(x1: Positive, x2: Even` – gpampara Jun 01 '15 at 09:09
  • Of course it's applicative, and you can use that interface as well. Why isn't it a monad? Can you prove you can't write a reasonable bind/join implementation for it? Because I fail to see how it's different from Maybe or Either String or Ecxeption or whatever name it's known under today. – n. m. could be an AI Jun 01 '15 at 09:16
  • 2
    @n.m. _very_ short answer: `Validation` is not a monad because it is defined this way in `scalaz`. For more details see for example http://stackoverflow.com/questions/12211776/why-isnt-validation-a-monad-scalaz7 – Michael Jun 01 '15 at 09:21
  • @gpampara What do you mean by "instance does not exist"? That it cannot be reasonably written, or that no one have bothered to write one? – n. m. could be an AI Jun 01 '15 at 09:22
  • @n.m. "instance does not exist" in this context means it does not exist in `scalaz`. – Michael Jun 01 '15 at 09:23
  • It may or msy not be defined in scalaz that way. I would have no idea. It is a monad regardless of any particular language or framework. – n. m. could be an AI Jun 01 '15 at 09:27
  • 2
    @n.m. `Validation` is not defined as a monad _for a reason_. You are welcome to read about it in any `scalaz` tutorial or ask on SO. – Michael Jun 01 '15 at 09:31
  • "means it does not exist in scalaz". Would not argue with that, though I wonder why it's not included. Anyway, the applicative interface is there and you are probably supposed to use it. – n. m. could be an AI Jun 01 '15 at 09:33
  • OK the reason seems to be that the applicative instance as would be implied by the monad is not the same as the applicative instance provided by scalaz. Sounds fair. – n. m. could be an AI Jun 01 '15 at 09:41
  • see this article for useful insight http://typelevel.org/blog/2014/02/21/error-handling.html – Gavin Jun 01 '15 at 09:48

2 Answers2

4

There are lots of ways to write this, but I like the following:

val checkX: X => Status[X] = x => isPositive(x.x1).tuple(isEven(x.x2)).as(x)

Or:

val checkX: X => Status[X] =
  x => isPositive(x.x1) *> isEven(x.x2) *> x.point[Status]

The key point is that you want to run the two validations only for their "effects" and then return the original value in the new context. This is a perfectly legitimate applicative operation, as your own implementation shows. There are just some slightly nicer ways to write it.

Travis Brown
  • 138,631
  • 12
  • 375
  • 680
  • Note that if you wanted to use the validated values (as in your implementation) you could write this: `x => (isPositive(x.x1) tuple isEven(x.x2)).map((X.apply _).tupled)`. I find the `as(x)` version clearer, though. – Travis Brown Jun 01 '15 at 14:58
0

It doesn't make sense, because the two functions can't really be composed. You'd (presumably) like to check whether x is positive and whether x is even - but in the case where it is both negative and odd, you'd like to receive both errors. But that can't ever happen as a composition of those two functions - once you apply either of the failing cases, you don't have an x any more to pass to the second function.

In my experience Validation is almost never the correct type to use, for this very reason.

If you want "fail-fast" behaviour where the result is either success or the first error, you should use \/ (i.e. type Status[A] = String \/ A in this case). If you want "accumulate all the error messages alongside the value" behaviour, you want Writer, i.e. type Status[A] = Writer[Vector[String], A]. Both of these types allow easy composition (because they have monad instances available) using e.g. Kleisli: Kleisli(isPositive) >==> isEven would work for either of these definitions of Status (but not for yours).

lmm
  • 17,386
  • 3
  • 26
  • 37
  • 1
    My `checkX` _does_ accumulate errors. You can try `checkX(new X(-1, 1))` in REPL and see the result `Status[X] = Failure(NonEmptyList(-1 not positive, 1 not even))`. – Michael Jun 01 '15 at 11:34
  • I don't want to use monads in this example. Applicative (which is not a monad) seems to fit better, – Michael Jun 01 '15 at 11:36
  • I agree that it's easy to overuse `Validation`, but there are lots of legitimate use cases, and this sounds like one of them. I don't understand how the objection in the first paragraph applies here. – Travis Brown Jun 01 '15 at 14:53
  • Ah - I missed that you were applying the function to different `x`s - in my defence you said "composition", which to my mind means applying one function to the result of another. In this case I think the "elegant" solution is to use a shapeless `Poly` that knows how to handle each field separately (i.e. `case class X(x1: Int @@ WidgetCount, x2: Int @@ FooCheck)`, and then have cases in your `Poly` for the two different types), and then use shapeless' `traverse` on case classes. (But that's a lot of machinery that may not be worth it). – lmm Jun 01 '15 at 15:21