Создание спецификатора 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] с нуля.

Может быть, даже лучше, если бы я мог написать:

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

Это намного меньше, чем решение Эрика, но не дает хорошего сообщения об ошибке. То, что я хотел бы иметь, - переименовать отображаемое значение во втором методе. Что-то вроде следующего (что не компилируется).

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

1 ответы

Если вы хотите написать что-то со второй версией, вам нужно создать новый класс Matcher , инкапсулирующий функциональные возможности beCloseTo :

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 в методе apply :

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]

Я не уверен, насколько более модульные мы можем получить с учетом ваших требований. Мне кажется, что лучшее, что мы можем сделать здесь, - это повторно использовать beCloseTo с помощью forall .

<�Сильный> UPDATE

Более короткий ответ может быть примерно таким:

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

Вы можете использовать foreach вместо forall , если вы хотите получить все сбои:

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
добавлено
Спасибо за ваш ответ Эрик. Ваше предложение работает, но в основном пишет новый помощник. Я добавил к моему вопросу более короткий способ создания совпадения (больше похоже на то, о чем я думал), но ему не хватает правильных сообщений.
добавлено автор ziggystar, источник
Есть ли шанс добавить эту функцию к «украшению» преобразования ^^ с чем-то вроде aka? В конце концов, имея ^^ (A => B) , вам нужно объяснить, как вы получили B из A , например, первый элемент `, если выполняется ^^ (_.head) для некоторой коллекции.
добавлено автор ziggystar, источник
pro.jvm
pro.jvm
3 503 участник(ов)

Сообщество разработчиков Java Scala Kotlin Groovy Clojure Чат для нач-их: @javastart Наш сайт: projvm.com projvm.ru Наш канал: @proJVM Вакансии: @jvmjobs Конфы: @jvmconf

Scala User Group
Scala User Group
1 486 участник(ов)

[RU] Scala Chat. Rules, additional links, FAQ: https://telegra.ph/Russian-Speaking-Scala-User-Group-08-27

Scala Jobs
Scala Jobs
852 участник(ов)

Rules: http://telegra.ph/My-lyudi-i-ehto-znachit-chto-nam-nuzhna-organizaciya-02-07 Main Scala Channel: https://t.me/scala_jobs_feed Flood Room: https://t.me/scala_ponv