Scala Тип данных для числового реального диапазона

Существует ли какой-нибудь идиоматический тип scala для ограничения значения с плавающей запятой заданного диапазона с плавающей запятой, который определяется верхней нижней границей?

Конкретный я хочу иметь тип float, которому разрешено иметь значения между 0.0 и 1.0.

Более конкретным я собираюсь написать функцию, которая принимает Int и другую функцию, которая отображает этот Int в диапазон между 0.0 и 1.0, в псевдо- scala:

def foo(x : Int, f : (Int => {0.0,...,1.0})) {
    // ....
}

Уже обыскали доски, но не нашли ничего подходящего. некоторые неявные магии или пользовательские typedef будет также хорошо для меня.

Ответ 1

Я бы не знал, как это сделать статически, за исключением зависимых типов (пример), который Scala не имеет. Если вы имеете дело только с константами, у вас должно быть возможно использование макросов или плагина компилятора, который выполняет необходимые проверки, но если у вас есть произвольные выражения с плавающей запятой, очень вероятно, что вам придется прибегать к проверке времени выполнения.

Вот подход. Определите класс, выполняющий проверку времени выполнения, чтобы убедиться, что значение поплавка находится в требуемом диапазоне:

abstract class AbstractRangedFloat(lb: Float, ub: Float) {
  require (lb <= value && value <= ub, s"Requires $lb <= $value <= $ub to hold")

  def value: Float
}

Вы можете использовать его следующим образом:

case class NormalisedFloat(val value: Float)
  extends AbstractRangedFloat(0.0f, 1.0f)

NormalisedFloat(0.99f)
NormalisedFloat(-0.1f) // Exception

Или как:

case class RangedFloat(val lb: Float, val ub: Float)(val value: Float)
  extends AbstractRangedFloat(lb, ub)

val RF = RangedFloat(-0.1f, 0.1f) _
RF(0.0f)
RF(0.2f) // Exception

Было бы неплохо, если бы можно было использовать классы значений, чтобы получить некоторую производительность, но вызов requires в конструкторе ( в настоящее время) запрещает это.


EDIT: адресация комментариев @paradigmatic

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

Теперь компьютерные поплавки и вещественные числа в конечном итоге являются конечными наборами, но все равно способными к тому, чтобы они были достаточно эффективно перечислимы в системе типов. Разумеется, множество компьютерных натуральных чисел также очень велико и, следовательно, создает проблему для арифметики по числам Пеано, закодированных как типы, см. Последний абзац в этой статье. Тем не менее, я утверждаю, что часто достаточно работать с первым n (для довольно малых n) натуральных чисел, как, например, подтверждается HLists. Создание соответствующей заявки на поплавки менее убедительно - было бы лучше кодировать 10 000 поплавков между 0.0 и 1.0 или, точнее, 10 000 между 0.0 и 100.0?

Ответ 2

Вот еще один подход, использующий неявный класс:

object ImplicitMyFloatClassContainer {

  implicit class MyFloat(val f: Float) {
    check(f)

    val checksEnabled = true

    override def toString: String = {
      // The "*" is just to show that this method gets called actually
      f.toString() + "*"
    }

    @inline
    def check(f: Float) {
      if (checksEnabled) {
        print(s"Checking $f")
        assert(0.0 <= f && f <= 1.0, "Out of range")
        println(" OK")
      }
    }

    @inline
    def add(f2: Float): MyFloat = {
      check(f2)

      val result = f + f2
      check(result)

      result
    }

    @inline
    def +(f2: Float): MyFloat = add(f2)
  }

}

object MyFloatDemo {
  def main(args: Array[String]) {
    import ImplicitMyFloatClassContainer._

    println("= Checked =")

    val a: MyFloat = 0.3f
    val b = a + 0.4f
    println(s"Result 1: $b")

    val c = 0.3f add 0.5f
    println("Result 2: " + c)

    println("= Unchecked =")

    val x = 0.3f + 0.8f
    println(x)

    val f = 0.5f
    val r = f + 0.3f
    println(r)

    println("= Check applied =")

    try {
      println(0.3f add 0.9f)
    } catch {
      case e: IllegalArgumentException => println("Failed as expected")
    }
  }
}

Для компилятора требуется подсказка использовать неявный класс, либо путем ввода явных указаний, либо путем выбора метода, который не предоставляется Scala Float.

Таким образом, по крайней мере, проверки централизованы, поэтому вы можете отключить их, если производительность является проблемой. Как указывал mhs, если этот класс преобразован в неявный класс значений, проверки должны быть удалены из конструктора.

Я добавил аннотации @inline, но я не уверен, если это полезно/необходимо для неявных классов.

Наконец, я не имел никакого успеха, чтобы unimport Scala Float "+" с

import scala.{Float => RealFloat}
import scala.Predef.{float2Float => _}
import scala.Predef.{Float2float => _}

возможно, есть еще один способ добиться этого, чтобы заставить компилятор использовать класс implict

Ответ 3

Вы можете использовать классы значений, указанные как mhs:

case class Prob private( val x: Double ) extends AnyVal {
  def *( that: Prob ) = Prob( this.x * that.x )
  def opposite = Prob( 1-x )
}

object Prob {
  def make( x: Double ) = 
    if( x >=0 && x <= 1 ) 
      Prob(x) 
    else 
      throw new RuntimeException( "X must be between 0 and 1" )
}

Они должны быть созданы с использованием метода factory в сопутствующем объекте, который будет проверять правильность диапазона:

scala> val x = Prob.make(0.5)
x: Prob = Prob(0.5)

scala> val y = Prob.make(1.1)
java.lang.RuntimeException: X must be between 0 and 1

Однако использование операций, которые никогда не будут выдавать число за пределами диапазона, не потребует проверки достоверности. Например * или opposite.