2016-11-11 22 views
2

我正在嘗試編寫用於在Scala中編寫系統測試的DSL。在這個DSL中,我不想公開某些操作可能異步發生的事實(因爲它們是使用被測試的Web服務實現的),或者可能發生錯誤(因爲Web服務可能不可用,我們希望測試失敗)。 In this answer這種方法令人沮喪,但我並不完全同意在編寫測試的DSL環境中。我認爲DSL會因這些方面的介紹而受到不必要的污染。用於系統測試的實用免費monads DSL:併發性和錯誤處理

在框架問題,請考慮以下DSL:

type Elem = String 

sealed trait TestF[A] 
// Put an element into the bag. 
case class Put[A](e: Elem, next: A) extends TestF[A] 
// Count the number of elements equal to "e" in the bag. 
case class Count[A](e: Elem, withCount: Int => A) extends TestF[A] 

def put(e: Elem): Free[TestF, Unit] = 
    Free.liftF(Put(e,())) 

def count(e: Elem): Free[TestF, Int] = 
    Free.liftF(Count(e, identity)) 

def test0 = for { 
    _ <- put("Apple") 
    _ <- put("Orange") 
    _ <- put("Pinneaple") 
    nApples <- count("Apple") 
    nPears <- count("Pear") 
    nBananas <- count("Banana") 
} yield List(("Apple", nApples), ("Pears", nPears), ("Bananas", nBananas)) 

現在假設我們要實現一個解釋器,利用我們的服務下試放和計數在店裏的元素。由於我們利用網絡,我希望put操作異步發生。另外,考慮到網絡錯誤或服務器錯誤可能發生,我希望程序在發生錯誤時立即停止。爲了說明我想實現的目標,here是一個通過單變換器(我無法翻譯爲Scala)在Haskell中混合不同方面的示例。

所以我的問題是,你會用滿足上述要求的解釋這單子M

def interp[A](cmd: TestF[A]): M[A] 

而如果M是單子變壓,你會如何使用的FP庫撰寫他們你的選擇(貓,斯卡拉茲)。

+1

'Task'(scalaz或更好FS2)應滿足所有的要求,它不需要單子變壓器,因爲它是已經有了要麼內(無論是FS2, \/for scalaz)。它也具有你需要的快速失敗行爲,與正確的偏向/異或行相同。 – dk14

+0

我不知道「Task」,很好。這種方法似乎也暗示了人們如何在Scala世界中構建monad,忘記Haskell中的'lift'運算符,定義您需要混合的所有方面(例如併發和錯誤處理)的自己的類,並定義一個monad實例。 –

+0

當使用'Task'時,你仍然需要解除,從值提升到Task,或者從Either到Task。但是,是的,它似乎比monad變換器更簡單,尤其是在monad幾乎不可組合的情況下(爲了定義monad變換器,除了作爲monad外,你還需要知道關於你的類型的一些其他細節 - 通常它需要像comonad提取價值)。僅僅爲了廣告的目的,我還要補充一點,'Task'表示堆棧安全的蹦牀計算。但是有一些項目着重於一元組合,比如'Emm-monad' – dk14

回答

1

Task(scalaz或更好FS2)應滿足所有的要求,它不需要單子變壓器,因爲它是已經Either內(Either對於FS2,\/爲scalaz)。它也具有您需要的快速失敗行爲,與右偏分離/異或相同。

以下是已知會我幾個實現:

不管單子變壓器沒有,你還在使用Task時還挺需要提升:

  • 從價值Task
  • EitherTask

但是,是的,它似乎比單子變壓器更簡單,尤其是單子幾乎不可組合的事實 - 爲了定義m onad變壓器,你必須知道一些關於你的類型的其他細節,除了是一個單子(通常它需要像comonad提取價值)。

僅用於廣告目的,我還會補充說Task表示堆棧安全的蹦牀計算。

不過,也有一些項目專注於擴展單子組成,像EMM-單子:https://github.com/djspiewak/emm,這樣你就可以撰寫單子變壓器與Future/TaskEitherOptionList等等等等。但是,國際海事組織與Applicative相比仍然有限 - cats提供了通用的Nested數據類型,可以輕鬆組成任何應用程序,您可以找到一些示例in this answer - 這裏唯一的缺點是使用Applicative很難構建可讀的DSL。另一種選擇是所謂的「自由單體」:https://github.com/m50d/paperdoll,它基本上提供了更好的構圖,並且允許將不同的效果層分成不同的解釋器。

例如,如沒有FutureT/變壓器你不能建立像type E = Option |: Task |: BaseOptionTask)的效果,例如flatMap將需要從Future/Task的值提取。

作爲一個結論,我可以說從我的經驗Task真的出現在基於do-notation的DSL中:我有一個複雜的外部規則 - 如異步計算的DSL,當我決定將它全部遷移到Scala-嵌入式版本Task真的幫助 - 我從字面上將外部DSL轉換爲Scala的for-comprehension。我們考慮的另一件事是自定義類型,例如ComputationRule,其中定義了一組類型類以及Task/Future或任何我們需要的轉換,但這是因爲我們沒有明確使用Free -monad。


你可能甚至不需要在這裏Free -monad假設你不需要切換口譯的能力(可能爲只是系統測試是真實的)。在這種情況下Task可能是你唯一需要的東西 - 這是懶惰的(與未來比較),真正做到了功能和堆棧安全:

trait DSL { 
    def put[E](e: E): Task[Unit] 
    def count[E](e: E): Task[Int] 
} 

object Implementation1 extends DSL { 

    ...implementation 
} 

object Implementation2 extends DSL { 

    ...implementation 
} 


//System-test script: 

def test0(dsl: DSL) = { 
    import dsl._ 
    for { 
    _ <- put("Apple") 
    _ <- put("Orange") 
    _ <- put("Pinneaple") 
    nApples <- count("Apple") 
    nPears <- count("Pear") 
    nBananas <- count("Banana") 
    } yield List(("Apple", nApples), ("Pears", nPears), ("Bananas", nBananas)) 
} 

所以,你可以通過不同的交換機執行「解釋」在這裏:

test0(Implementation1).unsafeRun 
test0(Implementation2).unsafeRun 

差異/缺點(與http://typelevel.org/cats/datatypes/freemonad.html比較):

  • 你堅持Task類型,所以你不能把它崩來點它很容易monad。
  • 實現在運行時通過DSL特性的實例(而不是自然轉換)解決,您可以使用eta-expansion:test0 _輕鬆進行抽象。 Java/Scala自然支持多態方法(put,count),但poly函數並不是那麼容易通過包含T => Task[Unit](對於put操作)的DSL實例,而不是使用自然變換DSLEntry ~> Task來生成合成多態函數DSLEntry[T] => Task[Unit]

  • 沒有明確AST作爲替代模式自然轉化內匹配 - 我們使用靜態調度(顯式調用的方法,這將返回懶計算)DSL特質裏面

事實上,你甚至可以擺脫這裏的Task

trait DSL[F[_]] { 
    def put[E](e: E): F[Unit] 
    def count[E](e: E): F[Int] 
} 

def test0[M[_]: Monad](dsl: DSL[M]) = {...} 

所以在這裏它甚至可能成爲首特別是當你不寫一個開放源碼庫的問題。

全部放在一起:

import cats._ 
import cats.implicits._ 

trait DSL[F[_]] { 
    def put[E](e: E): F[Unit] 
    def count[E](e: E): F[Int] 
} 

def test0[M[_]: Monad](dsl: DSL[M]) = { 
    import dsl._ 
    for { 
     _ <- put("Apple") 
     _ <- put("Orange") 
     _ <- put("Pinneaple") 
     nApples <- count("Apple") 
     nPears <- count("Pear") 
     nBananas <- count("Banana") 
    } yield List(("Apple", nApples), ("Pears", nPears), ("Bananas", nBananas)) 
} 

object IdDsl extends DSL[Id] { 
    def put[E](e: E) =() 
    def count[E](e: E) = 5 
} 

注意,貓有一個MonadId定義的,因此:

scala> test0(IdDsl) 
res2: cats.Id[List[(String, Int)]] = List((Apple,5), (Pears,5), (Bananas,5)) 

簡單的工作。當然,如果您願意,您可以選擇Task/Future/Option或任意組合。作爲事實上,你可以使用Applicative代替Monad

def test0[F[_]: Applicative](dsl: DSL[F]) = 
    dsl.count("Apple") |@| dsl.count("Pinapple apple pen") map {_ + _ } 

scala> test0(IdDsl) 
res8: cats.Id[Int] = 10 

|@|是一個平行的運營商,所以你可以使用cats.Validated代替Xor,注意|@|任務不執行(至少在舊的斯拉拉版本)並行(並行運算符不等於並行計算)。您也可以使用兩者的結合:

import cats.syntax._ 

def test0[M[_]:Monad](d: DSL[M]) = { 
    for { 
     _ <- d.put("Apple") 
     _ <- d.put("Orange") 
     _ <- d.put("Pinneaple") 
     sum <- d.count("Apple") |@| d.count("Pear") |@| d.count("Banana") map {_ + _ + _} 
    } yield sum 
} 

scala> test0(IdDsl) 
res18: cats.Id[Int] = 15