Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Testing laws of side-effecting monad

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?
like image 830
lambdas Avatar asked Sep 01 '14 09:09

lambdas


1 Answers

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.

like image 137
James_pic Avatar answered Sep 28 '22 00:09

James_pic