Реализация ценностного равенства функций - PullRequest
3 голосов
/ 04 мая 2019

Как переопределить equals, чтобы проверить эквивалентность значений функций в конкретных случаях?Например, скажем, у нас есть следующие функции f и g

val f = (x: Int) => "worf" + x
val g = (x: Int) => "worf" + x

Как мы могли бы сделать assert(f == g) проход?

Я попытался расширить Function1 и реализовал равенство черезгенератор, такой как

trait Function1Equals extends (Int => String) {
  override def equals(obj: Any): Boolean = {
    val b = obj.asInstanceOf[Function1Equals]
    (1 to 100).forall { _ =>
      val input = scala.util.Random.nextInt
      apply(input) == b(input)
    }
  }
}

implicit def functionEquality(f: Int => String): Function1Equals = (x: Int) => f(x)

, но не смог получить неявное преобразование для работы на ==, возможно, из-за this .TripleEquals от Scalactics подходит близко

import org.scalactic.TripleEquals._
import org.scalactic.Equality

implicit val functionEquality = new Equality[Int => String] {
  override def areEqual(a: Int => String, b: Any): Boolean =
    b match {
      case p: (Int => String) =>

        (1 to 100).forall { _ =>
          val input = scala.util.Random.nextInt
          a(input) == p(input)
        }

      case _ => false
    }
}

val f = (x: Int) => "worf" + x
val g = (x: Int) => "worf" + x
val h = (x: Int) => "picard" + x


assert(f === g) // pass
assert(f === h) // fail

Как бы вы реализовали равенство функций, предпочтительно используя обычный ==?

1 Ответ

5 голосов
/ 04 мая 2019

Прежде всего, равенство функций - это не простая тема (спойлер: это не может быть правильно реализовано; см., Например, этот вопрос и соответствующий ответ), но давайте предположим, что ваш метод "утверждения того же результата" на сто случайных входов "достаточно хорошо.

Проблема с переопределением == заключается в том, что он уже реализован для Function1 экземпляров. Итак, у вас есть два варианта:

  • определите пользовательскую черту (ваш подход) и используйте ==
  • определить класс типов с помощью операции isEqual и реализовать его для Function1

Оба варианта имеют компромиссы.

В первом случае вместо использования стандартной черты Scala Function1 вы должны вместо этого обернуть каждую функцию в свою собственную. Вы сделали это, но затем попытались реализовать неявное преобразование, которое будет выполнять преобразование из стандартного Function1 в Function1Equals для вас «за кадром». Но, как вы сами поняли, это не может работать. Зачем? Поскольку уже существует метод == для Function1 экземпляров, поэтому у компилятора нет причин запускать неявное преобразование. Вы должны обернуть каждый экземпляр Function1 в свою пользовательскую оболочку, чтобы вызывался переопределенный ==.

Вот пример кода:

trait MyFunction extends Function1[Int, String] {
  override def apply(a: Int): String
  override def equals(obj: Any) = {
    val b = obj.asInstanceOf[MyFunction]
    (1 to 100).forall { _ =>
      val input = scala.util.Random.nextInt
      apply(input) == b(input)
    }
  }
}

val f = new MyFunction {
  override def apply(x: Int) = "worf" + x 
}
val g = new MyFunction {
  override def apply(x: Int) = "worf" + x
}
val h = new MyFunction {
  override def apply(x: Int) = "picard" + x
}

assert(f == g) // pass
assert(f == h) // fail

Ваш второй вариант - продолжать работать со стандартными Function1 экземплярами, но использовать собственный метод для сравнения на равенство. Это может быть легко реализовано с использованием подхода класса типов:

  • определяет общую черту MyEquals[A], которая будет иметь необходимый метод (назовем его isEqual)
  • определяет неявное значение, которое реализует эту черту для Function1[Int, String]
  • определяет вспомогательный неявный класс, который предоставит метод isEqual для некоторого значения типа A, пока существует неявная реализация MyEquals[A] (и на предыдущем шаге мы убедились, что для MyEquals[Function1[Int, String]])

Тогда код выглядит так:

trait MyEquals[A] {
  def isEqual(a1: A, a2: A): Boolean
}

implicit val function1EqualsIntString = new MyEquals[Int => String] {
  def isEqual(f1: Int => String, f2: Int => String) =
    (1 to 100).forall { _ =>
      val input = scala.util.Random.nextInt
      f1(input) == f2(input)
   }
}

implicit class MyEqualsOps[A: MyEquals](a1: A) {
  def isEqual(a2: A) = implicitly[MyEquals[A]].isEqual(a1, a2)
}

val f = (x: Int) => "worf" + x
val g = (x: Int) => "worf" + x
val h = (x: Int) => "picard" + x

assert(f isEqual g) // pass
assert(f isEqual h) // fail

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

Это хорошая демонстрация того, почему классы типов полезны и более мощны, чем наследование. Вместо того, чтобы наследовать == от некоторого объекта суперкласса и переопределять его, что проблематично для типов, которые мы не можем изменить (например, Function1), вместо этого должен быть класс типов (давайте назовем его Equal), который обеспечивает метод равенства для много типов.

Так что, если неявный экземпляр Equal[Function1] еще не существует в области действия, мы просто предоставляем свой собственный (как мы сделали в моем втором фрагменте), и компилятор будет его использовать. С другой стороны, если неявный экземпляр Equal[Function1] уже где-то существует (например, в стандартной библиотеке), он ничего не меняет для нас - нам все равно нужно просто предоставить наш собственный, и он «переопределит» существующий.

А теперь самое интересное: такой класс типов уже существует как в scalaz , так и в cats . Он называется Equal и Eq соответственно, и они оба назвали свой метод сравнения на равенство ===. Вот почему я сказал ранее, что даже не буду рассматривать возможность использования == в качестве преимущества. Кому нужно == в любом случае? :) Использование скаляр или котов в вашей кодовой базе будет означать, что вы будете полагаться на === вместо == везде, и ваша жизнь будет простой (r).

Но не рассчитывайте на равенство функций; все это требование странно и не хорошо. Я ответил на ваш вопрос, притворяясь, что это хорошо для того, чтобы дать некоторую информацию, но лучший ответ был бы - вообще не полагаться на равенство функций.

...