モジュラー形式でSpecs2マッチャーを作成する

I have functions A => Double. I want to check whether two such functions give the same results (up to a tolerance, using the existing beCloseTo matcher) for a given set of values.

私は書くことができるようにしたい:

type TF = A => Double
(f: TF) must computeSameResultsAs(g: TF,tolerance: Double, tests: Set[A])

私は単純に Matcher [TF] を書くのではなく、モジュール式にこのmatcherを構築したいと考えています。

もし私が書くことができればさらに良いかもしれません:

(f: TF) must computeSameResultsAs(g: TF)
               .withTolerance(tolerance)
               .onValues(tests: Set[A])

また、マッチャーが失敗したときに合理的な説明を得たいと思います。

編集

それを眠った後、私は次のことを思いついた。

def computeSameResultsAs[A](ref: A => Double, tolerance: Double, args: Set[A]): Matcher[A => Double] = 
  args.map(beCloseOnArg(ref, tolerance, _)).reduce(_ and _)

def beCloseOnArg[A](ref: A => Double, tolerance: Double, arg: A): Matcher[A => Double] = 
  closeTo(ref(arg), tolerance) ^^ ((_: A => Double).apply(arg))

これはEricのソリューションよりもはるかに短いですが、良い失敗メッセージを提供しません。私ができることが大好きなのは、2番目の方法でマップされた値の名前を変更することです。以下のようなもの(コンパイルされないもの)。

def beCloseOnArg[A](ref: A => Double, tolerance: Double, arg: A): Matcher[A => Double] = 
  closeTo(ref(arg), tolerance) ^^ ((_: A => Double).apply(arg) aka "result on argument " + arg)
8
追加された 編集された
ビュー: 2

1 答え

2番目のバージョンで物事を書く場合は、 beCloseTo マッチャーの機能をカプセル化する新しい Matcher クラスを作成する必要があります。

def computeSameResultsAs[A](g: A => Double, 
                            tolerance: Double = 0.0, 
                            values: Seq[A] = Seq()) = TFMatcher(g, tolerance, values)

case class TFMatcher[A](g: A => Double, 
                        tolerance: Double = 0.0, 
                        values: Seq[A] = Seq()) extends Matcher[A => Double] {

  def apply[S <: A => Double](f: Expectable[S]) = {
   //see definition below
  }

  def withTolerance(t: Double) = TFMatcher(g, t, values)
  def onValues(tests: A*) = TFMatcher(g, tolerance, tests)
}

このクラスでは、後の構文を使用できます。

val f = (i: Int) => i.toDouble
val g = (i: Int) => i.toDouble + 0.1

"f must be close to another similar function with a tolerance" in {
  f must computeSameResultsAs[Int](g).withTolerance(0.5).onValues(1, 2, 3)          
}

次に、適用メソッドで beCloseTo マッチャーを再利用する方法を見てみましょう。

def apply[S <: A => Double](f: Expectable[S]) = {
  val res = ((v: A) => beCloseTo(g(v) +/- tolerance).apply(theValue(f.value(v)))).forall(values)

  val message = "f is "+(if (res.isSuccess) "" else "not ")+
                "close to g with a tolerance of "+tolerance+" "+
                "on values "+values.mkString(",")+": "+res.message
   result(res.isSuccess, message, message, f)
 }

上記のコードでは、 MatcherResult を値のシーケンスに関連付けます

((v: A) => beCloseTo(g(v) +/- tolerance).apply(theValue(f.value(v)))).forall(values)

ご了承ください:

  1. f is an Expectable[A => Double] so we need to take its actual value to be able to use it

  2. similarly we can only apply an Expectable[T] to a Matcher[T] so we need to use the method theValue to transform f.value(v) to an Expectable[Double] (from the MustExpectations trait)

最後に、 forall の結果が得られたら、次のようにして結果メッセージをカスタマイズすることができます:

  1. the inherited result method building a MatchResult (what the apply method of any Matcher should return

  2. passing it a boolean saying if the execution of beCloseTo was successful: .isSuccess

  3. passing it nicely formatted "ok" and "ko" messages, based on the input and on the result message of the beCloseTo matching

  4. passing it the Expectable which was used to do the matching in the first place: f, so that the final result has a type of MatchResult[A => Double]

私はあなたの要求をどのようにモジュラー化できるかはわかりません。ここでできることは、 forbeCloseTo を再利用することです。

更新

短い答えは次のようなものかもしれません:

val f = (i: Int) => i.toDouble
val g = (i: Int) => i.toDouble + 1.0

"f must be close to another similar function with a tolerance" in {
  f must computeSameResultsAs[Int](g, tolerance = 0.5, values = Seq(1, 2, 3))          
}

def computeSameResultsAs[A](ref: A => Double, tolerance: Double, values: Seq[A]): Matcher[A => Double] = (f: A => Double) => {
  verifyFunction((a: A) => (beCloseTo(ref(a) +/- tolerance)).apply(theValue(f(a)))).forall(values)
}

上のコードは、次のような失敗メッセージを生成します。

In the sequence '1, 2, 3', the 1st element is failing: 1.0 is not close to 2.0 +/- 0.5

This should almost work out-of-the-box. The missing part is an implicit conversion from A => MatchResult[_] to Matcher[A] (which I'm going to add to the next version):

implicit def functionResultToMatcher[T](f: T => MatchResult[_]): Matcher[T] = (t: T) => {
  val result = f(t)
  (result.isSuccess, result.message)
}

すべての失敗を取得する場合は、 forall の代わりに foreach を使用します。

1.0 is not close to 2.0 +/- 0.5; 2.0 is not close to 3.0 +/- 0.5; 3.0 is not close to 4.0 +/- 0.5

更新2

これは毎日良くなります。最新のspecs2スナップショットを使用すると、次のように記述できます。

def computeSameResultsAs[A](ref: A => Double, tolerance: Double, values: Seq[A]): Matcher[A => Double] = (f: A => Double) => {
  ((a: A) => beCloseTo(ref(a) +/- tolerance) ^^ f).forall(values)
}   

更新3

最新のspecs2スナップショットを使用すると、次のように記述できます。

def computeSameResultsAs[A](ref: A => Double, tolerance: Double, values: Seq[A]): Matcher[A => Double] = (f: A => Double) => {
  ((a: A) => beCloseTo(ref(a) +/- tolerance) ^^ ((a1: A) => f(a) aka "the value")).forall(values)
}   

失敗メッセージは次のようになります。

In the sequence '1, 2, 3', the 1st element is failing: the value '1.0' is not close to 2.0 +/- 0.5
9
追加された
あなたの答えエリックに感謝します。あなたの提案は機能しますが、基本的には新しいマッチャーを作成します。私は私の質問にmatcherを作成するための短い方法を追加しましたが(私が思ったものに近い)、適切なメッセージがありません。
追加された 著者 ziggystar,
" ^^ 変換を別名のように「装飾」する機能を追加する機会はありますか?結局のところ、 ^^(A => B)があるときは、 A から B いくつかのコレクションに対して ^^(_.head)を実行する場合は `の最初の要素です。
追加された 著者 ziggystar,