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?
lambdas
  • 3,990
  • 2
  • 29
  • 54
  • 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 `ApiAction`s as derived case classes and case objects. – Karol S Sep 01 '14 at 19:35

3 Answers3

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.

James_pic
  • 3,240
  • 19
  • 24
  • 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. – lambdas Sep 05 '14 at 14:25
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.

johanandren
  • 11,249
  • 1
  • 25
  • 30
  • Then it is impossible to define monad on `ApiAction` since `point` is undefined(you can't create instances of an abstract class). – lambdas Sep 03 '14 at 19:47
  • 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) } – johanandren Sep 04 '14 at 08:28
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?

Community
  • 1
  • 1
samthebest
  • 30,803
  • 25
  • 102
  • 142
  • 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;) – lambdas Sep 03 '14 at 11:01