2014-09-01 46 views
7

我正在編寫一個庫來通過API訪問Web服務。我定義簡單的類來表示API行動測試副作用monad的法律

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

和執行Web服務的一些功能調用

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

,我也做了ApiAction單子所以

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

我可以做類似

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

現在我有麻煩測試它的單子法律

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

因爲monadLaw.rightIdentity使用equal

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

並沒有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]           ^

問題是,我甚至無法想象它怎麼可能定義Equal[ApiAction]ApiAction是本質上的一個函數,我不知道任何功能上的平等關係。當然,可以比較運行ApiAction的結果,但它是不一樣的。

我覺得我在做一些非常錯誤的事情,或者不明白某些重要的東西。所以我的問題是:

  • 是否有意義的ApiAction是一個monad?
  • 我設計了ApiAction對不對?
  • 我應該如何測試它的monad法則?
+0

對無限域函數沒有可計算的等式關係。你可以運行一個密封特徵的抽象方法,然後將你的ApiAction作爲派生的case類和case對象來實現。 – 2014-09-01 19:35:55

回答

4

我將從簡單的開始:是的,它是有意義的,因爲ApiAction是一個monad。是的,你已經以合理的方式設計它 - 這種設計看起來有點像哈斯克爾的IO monad。

棘手的問題是你應該如何測試它。

唯一有意義的等式關係是「在給定相同輸入的情況下產生相同的輸出」,但這隻對紙張纔有用,因爲它不可能用於計算機驗證,而且它只對純函數有意義。確實,Haskell的monod,與你的monad有一些相似之處,並沒有實現Eq。所以,如果你沒有實施Equal[ApiAction],你可能會在安全的基礎上。

不過,可能會有一個參數用於實現僅用於測試的特殊Equal[ApiAction]實例,該實例使用硬編碼的Credentials值(或少量硬編碼值)運行該操作並對結果進行比較。從理論的角度來看,這很糟糕,但從實用的角度來看,它並不比測試用例更差,並且可以讓您重用Scalaz中現有的幫助函數。

另一種方法是忘記斯卡拉茲,證明ApiAction使用鉛筆紙滿足單子法,然後編寫一些測試用例來驗證一切是否按照您認爲的方式工作(使用您所使用的方法寫的,而不是斯卡拉茲的)。事實上,大多數人會跳過鉛筆和紙張的步驟。

+0

很好的回答!你說得對,只有純函數纔有意義,'apiAction.run == apiAction.run'通常不會因爲副作用而存在,並且使用鉛筆和紙張來證明規律,我們應該假設這種行爲從未失敗。 – lambdas 2014-09-05 14:25:34

0

我認爲你可以實現它的不利之處在於不得不使用宏或反射來將你的函數包含到包含AST的類中。然後你可以通過比較兩個函數來比較它們的AST。

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

+0

我不認爲這是一個好的解決方案。我只是試圖以功能性的方式設計圖書館,並認爲我做錯了。不管怎樣,謝謝你。關聯你的答案 - 可以用不同的方式實現具有相同行爲的函數;) – lambdas 2014-09-03 11:01:58

+0

是的,但這意味着你的代碼會變溼。 – samthebest 2014-09-04 07:40:34

1

它歸結爲lambda表達式是功能N,在這裏你只需要實例平等的匿名子類,所以纔有了相同anonymus子類是與自己平等的。

你如何能做到這一點的一個想法: 讓您的操作功能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'上定義monad。 – lambdas 2014-09-03 19:47:20

+0

但是你可以爲它創建一個標識/點?案例類點(a:A)擴展ApiAction [A] {def apply(creds:Credentials):要麼[Error,Foo] = Right(a)} – johanandren 2014-09-04 08:28:25

+0

我沒有想到,你是對的! – lambdas 2014-09-04 09:41:51