2014-09-01 5 views
7

API를 통해 웹 서비스에 액세스하기위한 라이브러리를 작성하고 있습니다. 나는 API 액션부작용 모나드의 테스트 법칙

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

을 나타내는 간단한 클래스를 정의한 웹 서비스를 수행 일부 기능이 나는 또한

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

그래서 내가 할 수있는 ApiAction 모나드했습니다

// 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] = ??? 

를 호출 do like something

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

는 지금은 monadLaw.rightIdentityequal

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

를 사용하고 더 Equal[ApiAction]이없는

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) 
} 

때문에 문제가 모나드 법을 테스트합니다.

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

문제는 내가 어떻게 Equal[ApiAction]을 정의 할 수 있는지 상상할 수 없다는 것입니다. ApiAction은 필수 기능이며 기능에 대한 동등한 관계를 알지 못합니다. 물론 ApiAction '을 실행 한 결과를 비교하는 것도 가능하지만 동일하지는 않습니다.

뭔가 잘못하고있는 것처럼 느껴지거나 필수적인 것을 이해하지 못합니다. 따라서 내 질문은 다음과 같습니다.

  • ApiAction이 모나드가되는 것이 맞습니까?
  • ApiAction을 올바르게 디자인 했습니까?
  • 모나드 법을 어떻게 테스트해야합니까?
+0

무한 도메인이있는 함수에 대해 계산 가능한 동등 관계가 없습니다. 당신은 봉인 된 형질의 추상 메소드를'run'으로 만들 수 있고,'ApiAction'을 파생 된 케이스 클래스와 케이스 객체로 구현할 수 있습니다. –

답변

4

쉬운 것들부터 시작하겠습니다 : 예, ApiAction이 모나드가되는 것이 맞습니다. 그리고 예, 합리적인 방법으로 설계했습니다.이 디자인은 Haskell의 IO 모나드와 약간 비슷해 보입니다.

까다로운 질문은 테스트 방법입니다.

의미가있는 유일한 동등 관계는 "동일한 입력이 주어진 동일한 출력을 생성합니다"이지만 컴퓨터에서 확인할 수 없기 때문에 종이에 유용합니다. 순수한 함수에만 의미가 있습니다. 실제로 Haskell의 IO 모나드는 모나드와 유사하기 때문에 Eq을 구현하지 않습니다. 따라서 Equal[ApiAction]을 구현하지 않으면 안전 할 수 있습니다.

여전히 인스턴스를 하드 코드 된 값 (또는 하드 코딩 된 값의 수가 적음)으로 실행하고 테스트 결과로 비교할 수있는 특별한 Equal[ApiAction] 인스턴스를 구현하기위한 인수가있을 수 있습니다. 이론적 인 관점에서 볼 때 실제로는 끔찍한 것이지만 실용적인 관점에서 보면 테스트 케이스로 테스트하는 것보다 나쁘지 않으며 Scalaz의 기존 도우미 기능을 다시 사용할 수 있습니다.

다른 접근법은 Scalaz를 잊어 버리고 연필과 종이를 사용하여 모나드 법칙을 충족시키는 것을 증명하고 (모든 방법이 생각대로 작동하는지 확인하기 위해 몇 가지 테스트 케이스를 작성합니다. Scalaz 출신이 아닙니다.) 사실, 대부분의 사람들은 연필과 종이 단계를 건너 뛸 것입니다.

+0

좋은 답변입니다! ApiAction.run은 apiAction.run == apiAction.run'은 부작용으로 인해 일반적으로 보류하지 않으며 연필과 종이를 사용하는 법을 증명하기 위해 우리는 그 행동을 가정해야합니다. 절대 실패하지 않습니다. – lambdas

0

매크로 나 반사를 사용하여 함수를 AST가 포함 된 클래스로 래핑해야하는 단점을 가지고 구현할 수 있다고 생각합니다. 그런 다음 AST를 비교하여 두 기능을 비교할 수 있습니다.

지금은 같은 anonymus 서브 클래스 자체와 동일, 그것은 당신 만 인스턴스 평등을 FunctionN의 익명의 서브 클래스 인 람다 아래로 비등 What's the easiest way to use reify (get an AST of) an expression in Scala?

+0

좋은 해결책이라고 생각하지 않습니다. 나는 기능적인 방식으로 라이브러리를 디자인하려고하고 있으며 내가 잘못하고 있다고 생각하고 있습니다. 어쨌든 고맙습니다. 당신의 대답을 관련 시키십시오 - 다른 행동으로 동일한 행동으로 기능을 구현하는 것이 가능합니다;) – lambdas

+0

사실,하지만 그건 당신의 코드가 젖어 있다는 것을 의미합니다. – samthebest

1

를 참조하십시오.

당신이 그것을 할 수있는 방법의 하나 개의 아이디어 : 당신의 작업을 구체적인 기능 1의 서브 클래스 대신 인스턴스 확인 : 예를 들어이 경우 귀하의 인스턴스에 대한 객체 생성을 위해 당신을 허용 할

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

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

이 차례로 ApiAction의 각 하위 클래스에 대해 equals를 구현할 수 있어야합니다 (예 : 생성자의 매개 변수).

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

당신은 또한 내가 같은 기능 1을 확장하지 않고이 작업을 수행 할 수있다 (당신은 내가했던 것처럼, 그 자유로운 결정을위한 작업의 경우 클래스를 얻을 수)했고, 당신이했던 것처럼 실행 필드를 사용하지만,이 방법은 대부분의 준 succint 예제 코드.

+0

그러면'point'가 정의되지 않았기 때문에'ApiAction'에 모나드를 정의 할 수 없습니다 (추상 클래스의 인스턴스를 만들 수는 없습니다). – lambdas

+0

하지만 ID/포인트를 만들 수 있습니까? [오류, Foo] = 오른쪽 (a)} – johanandren

+0

나는 그것을 생각하지 않았습니다, 당신 말이 맞습니다! (A : A) ApiAction [A] {def apply (creds : Credentials) – lambdas