Testing laws of side-effecting monad
Asked Answered
F

3

7

I'm writing a library to access web service through the API. I've defined simple class to represent API action

case class ApiAction[A](run: Credentials => Either[Error, A])

and some functions that performs web service calls

// Retrieve foo by id
def get(id: Long): ApiAction[Foo] = ???

// List all foo's
def list: ApiAction[Seq[Foo]] = ???

// Create a new foo
def create(name: String): ApiAction[Foo] = ???

// Update foo
def update(updated: Foo): ApiAction[Foo] = ???

// Delete foo
def delete(id: Long): ApiAction[Unit] = ???

I've also made ApiAction a monad

implicit val monad = new Monad[ApiAction] { ... }

So I could do something like

create("My foo").run(c)
get(42).map(changeFooSomehow).flatMap(update).run(c)
get(42).map(_.id).flatMap(delete).run(c)

Now I have troubles testing its monad laws

val x = 42
val unitX: ApiAction[Int] = Monad[ApiAction].point(x)

"ApiAction" should "satisfy identity law" in {
  Monad[ApiAction].monadLaw.rightIdentity(unitX) should be (true)
}

because monadLaw.rightIdentity uses equal

def rightIdentity[A](a: F[A])(implicit FA: Equal[F[A]]): Boolean = 
  FA.equal(bind(a)(point(_: A)), a)

and there is no Equal[ApiAction].

[error] could not find implicit value for parameter FA: scalaz.Equal[ApiAction[Int]]
[error]     Monad[ApiAction].monadLaw.rightIdentity(unitX) should be (true)
[error]                                            ^

The problem is I can't even imagine how it could be possible to define Equal[ApiAction]. ApiAction is essentialy a function, and I don't know of any equality relation on functions. Of course it is possible to compare results of running ApiAction's, but it is not the same.

I feel as I doing something terribly wrong or don't understand something essential. So my questions are:

  • Does it makes sense for ApiAction to be a monad?
  • Have I designed ApiAction right?
  • How should I test its monad laws?
Forgotten answered 1/9, 2014 at 9:48 Comment(1)
There is no computable equality relation for functions with infinite domain. You could make run an abstract method of a sealed trait, and then implement your ApiActions as derived case classes and case objects.Snapper
B
4

I'll start with the easy ones: Yes, it makes sense for ApiAction to be a monad. And yes, you've designed it in a reasonable way - this design looks a bit like the IO monad in Haskell.

The tricky question is how you should test it.

The only equality relation that makes sense is "produces same output given same input", but that's only really useful on paper, since it's not possible for a computer to verify, and it's only meaningful for pure functions. Indeed, Haskell's IO monad, which has some similarities to your monad, doesn't implement Eq. So you're probably on safe ground if you don't implement Equal[ApiAction].

Still, there might be an argument for implementing a special Equal[ApiAction] instance for use solely in tests, that runs the action with a hard-coded Credentials value (or a small number of hard-coded values) and compares the results. From a theoretical point of view, it's just awful, but from a pragmatic point of view it's no worse than testing it with test cases, and lets you re-use existing helper functions from Scalaz.

The other approach would be to forget about Scalaz, prove ApiAction satisfies the monad laws using pencil-and-paper, and write some test cases to verify that everything works the way you think it does (using the methods you've written, not the ones from Scalaz). Indeed, most people would skip the pencil-and-paper step.

Bromide answered 5/9, 2014 at 13:32 Comment(1)
Great answer! You're so right that equality make sense only for pure functions, apiAction.run == apiAction.run doesn't hold in general because of side effects, and to prove laws using pencil-and-paper we should suppose that action never fails.Forgotten
R
1

It boils down to lambdas being anonymous subclasses of FunctionN, where you only have instance equality, so only the same anonymus subclass is equal with itself.

One idea of how you could do it: Make your operations concrete subclasses of Function1 instead of instances:

abstract class ApiAction[A] extends (Credentials => Either[Error, A])
// (which is the same as)
abstract class ApiAction[A] extends Function1[Credentials, Either[Error, A]]

Which would allow you to for example create case objects for your instances

case class get(id: Long) extends ApiAction[Foo] {
  def apply(creds: Credentials): Either[Error, Foo] = ... 
}

and this in turn should allow you to implement equals for each subclass of ApiAction in a way that suits you, for example on the parameters of the constructor. (You could get that for free making the operations case classes, like i did)

val a = get(1)
val b = get(1)
a == b

You could also do this without extending Function1 like I did and use a run field like you did, but this way gave the most succint example code.

Reta answered 3/9, 2014 at 19:31 Comment(2)
Then it is impossible to define monad on ApiAction since point is undefined(you can't create instances of an abstract class).Forgotten
But you could create an identity/point for it? case class point(a: A) extends ApiAction[A] { def apply(creds: Credentials): Either[Error, Foo] = Right(a) }Reta
A
0

I think you could implement it with the downside of having to use a macro or reflection to wrap your functions into a class that includes an AST. You can then compare two functions by comparing their AST.

See What's the easiest way to use reify (get an AST of) an expression in Scala?

Acidulate answered 2/9, 2014 at 13:49 Comment(1)
I don't think it is a good solution. I'm just trying to design library in a functional way and thinking that I'm doing it wrong. Thank you anyway. Relating your answer - it is possible to implement functions with identical behaviour in a different ways;)Forgotten

© 2022 - 2024 — McMap. All rights reserved.