12
votes

Is there some idiomatic scala type to limit a floating point value to a given float range that is defined by a upper an lower bound?

Concrete i want to have a float type that is only allowed to have values between 0.0 and 1.0.

More concrete i am about to write a function that takes a Int and another function that maps this Int to the range between 0.0 and 1.0, in pseudo-scala:

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

Already searched the boards, but found nothing appropriate. some implicit-magic or custom typedef would be also ok for me.

3
Have you looked at spire?rsenna
looks promising, will check it out!the dude

3 Answers

8
votes

I wouldn't know how to do it statically, except with dependent types (example), which Scala doesn't have. If you only dealt with constants it should be possible to use macros or a compiler plug-in that performs the necessary checks, but if you have arbitrary float-typed expressions it is very likely that you have to resort to runtime checks.

Here is an approach. Define a class that performs a runtime check to ensure that the float value is in the required range:

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

  def value: Float
}

You could use it as follows:

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

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

Or as:

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

It would be nice if one could use value classes in order to gain some performance, but the call to requires in the constructor (currently) prohibits that.


EDIT : addressing comments by @paradigmatic

Here is an intuitive argument why types depending on natural numbers can be encoded in a type system that does not (fully) support dependent types, but ranged floats probably cannot: The natural numbers are an enumerable set, which makes it possible to encode each element as path-dependent types using Peano numerals. The real numbers, however, are not enumerable any more, and it is thus no longer possible to systematically create types corresponding to each element of the reals.

Now, computer floats and reals are eventually finite sets, but still way to large to be reasonably efficiently enumerable in a type system. The set of computer natural numbers is of course also very large and thus poses a problem for arithmetic over Peano numerals encoded as types, see the last paragraph of this article. However, I claim that it is often sufficient to work with the first n (for a rather small n) natural numbers, as, for example, evidenced by HLists. Making the corresponding claim for floats is less convincing - would it be better to encode 10,000 floats between 0.0 and 1.0, or rather 10,000 between 0.0 and 100.0?

2
votes

Here is another approach using an implicit class:

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

It requires a hint for the compiler to use the implicit class, either by typing the summands explicitly or by choosing a method which is not provided by Scala's Float.

This way at least the checks are centralized, so you can turn it off, if performance is an issue. As mhs pointed out, if this class is converted to an implicit value class, the checks must be removed from the constructor.

I have added @inline annotations, but I'm not sure, if this is helpful/necessary with implicit classes.

Finally, I have had no success to unimport the Scala Float "+" with

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

possibly there is another way to achieve this in order to push the compiler to use the implict class

2
votes

You can use value classes as pointed by 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" )
}

They must be created using the factory method in the companion object, which will check that the range is correct:

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

However using operations that will never produce a number outside the range will not require validity check. For instance * or opposite.