2

Scala でシステム テストを記述するための DSL を作成しようとしています。この DSL では、一部の操作が非同期で行われる可能性がある (たとえば、テスト対象の Web サービスを使用して実装されているため)、またはエラーが発生する可能性がある (Web サービスが利用できない可能性があるため) という事実を公開したくありません。 、そしてテストを失敗させたい)。この回答では、このアプローチはお勧めできませんが、テストを書くための DSL のコンテキストでは、これに完全には同意しません。これらの側面の導入により、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ネットワークを利用するので、非同期で運用してほしい。また、ネットワークエラーやサーバーエラーが発生する可能性があるため、エラーが発生したらすぐにプログラムを停止してほしい。私が何を達成したいのかを理解するために、モナド変換子 (Scala に変換できない) を使用して Haskell のさまざまな側面を混合する例を次に示します

Mだから私の質問は、上記の要件を満たすインタープリターにどのモナドを使用するかということです:

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

M がモナド変換器の場合、選択した FP ライブラリ (Cats、Scalaz) を使用してそれらをどのように構成しますか。

4

1 に答える 1

1

TaskEither(scalaz またはより良い fs2) は、すべての要件を満たす必要があります。既に内部にあるため、monad -transformer は必要ありません( Eitherfs2 の場合\/、scalaz の場合)。また、右バイアス分離/xor と同じように、必要なファスト フェイル動作も備えています。

私が知っているいくつかの実装を次に示します。

モナドトランスフォーマーの不在に関係なく、使用するときはまだ持ち上げる必要がありますTask:

  • 値からTaskまたは
  • からEitherまでTask

しかし、はい、特にモナドはほとんど構成可能ではないという事実に関しては、モナド変換子よりも単純であるように思われます-モナド変換子を定義するには、モナドであることに加えて、型に関する他の詳細を知る必要があります(通常、それには comonad のようなものが必要です)値を抽出します)。

宣伝目的で、Taskスタックセーフなトランポリン計算を表す も追加​​します。

ただし、Emm-monad: https://github.com/djspiewak/emmなど、拡張されたモナド構成に焦点を当てたプロジェクトがいくつかあるため、 Future/ TaskEitherOptionなどでモナド変換子を構成できますList。しかし、IMO、それは構成と比較してまだ制限されてApplicativeいます-Applicativeを簡単に構成できるcatsユニバーサルデータ型を提供します。この回答でNestedいくつかの例を見つけることができます-ここでの唯一の欠点は、Applicativeを使用して読み取り可能なDSLを構築するのが難しいことです. 別の代替手段は、いわゆる「Freer モナド」です: https://github.com/m50d/paperdoll、これは基本的により良い構成を提供し、異なるエフェクトレイヤーを異なるインタープリターに分離することを可能にします.

たとえば、FutureT/トランスフォーマーがないため、 ( from ) のTaskTような効果を構築することはできません。これは、 /から値を抽出する必要があるためです。type E = Option |: Task |: BaseOptionTaskflatMapFutureTask

結論として、私の経験から言えば、Taskdo 記法ベースの DSL に本当に適していると言えます。非同期計算用の複雑な外部ルールのような DSL があり、それをすべて Scala 組み込みバージョンに移行することにしたとき、Task本当に役に立ちました -私は文字通り external-DSL を Scala の に変換しましfor-comprehensionた。私たちが検討したもう 1 つのことは、/または必要なものComputationRuleへの変換とともに定義された型クラスのセットなど、いくつかのカスタム型を持つことですが、これは-monadを明示的に使用しなかったためです。TaskFutureFree


Freeインタープリターを切り替える機能が必要ないと仮定すると、ここでは -monadさえ必要ないかもしれません(これはシステムテストだけに当てはまるかもしれません)。その場合Task、あなたが必要とする唯一のものかもしれません - それは(Futureと比較して)怠惰で、真に機能的でスタックセーフです:

 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いるため、他のモナドに簡単に折りたたむことはできません。
  • 実装は、(自然な変換ではなく) DSL-trait のインスタンスを渡すと実行時に解決されるため、 eta-expansion: を使用して簡単に抽象化できますtest0 _。ポリモーフィック メソッド (put、count) は Java/Scala で自然にサポートされていますが、ポリ関数はサポートされていないため、natural-transform を使用して合成ポリモーフィック関数を作成するよりも(操作のために) コンテナーのインスタンスDSLを 渡す方が簡単です。T => Task[Unit]putDSLEntry[T] => Task[Unit]DSLEntry ~> Task

  • 自然な変換内のパターン マッチングの代わりに明示的な 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
}

ネコにはMonadfor が定義されていることに注意してくださいId

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は並列演算子なので、の代わりに使用できますが、 for Task は (少なくとも古いバージョンの scalaz では) 並列で実行されない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
于 2016-11-12T04:22:29.147 に答える