12

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.

om-nom-nom
  • 62,329
  • 13
  • 183
  • 228
the dude
  • 799
  • 9
  • 26

3 Answers3

8

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?

Malte Schwerhoff
  • 12,684
  • 4
  • 41
  • 71
  • 3
    It's worth noting that even if Scala did have dependent types that could express this, it would come at a huge cost. People who want restricted types often don't think of the added burden of producing values of those types. Someone gives you an input float? Make sure you have a testing procedure that either produces a float in your range or tells you it doesn't fit. Doing complicated math on a collection of floats whose result can be shown to fit? You'll need to write a long proof (in the language) that the result will always fit in the range. I personally love doing it, but it's hard... – Mysterious Dan May 16 '13 at 13:38
  • There are depend types in scala. Look at first answer: http://stackoverflow.com/questions/12935731/any-reason-why-scala-does-not-explicitly-support-dependent-types – paradigmatic May 17 '13 at 09:06
  • @paradigmatic Ever tried to encode ranged floats (not nats) with what Scala offers in that respect? Let me know if you succeed. – Malte Schwerhoff May 17 '13 at 09:31
  • @mhs never tried (and never will). But despite limitations there are dependent types in Scala and pretending the opposite is wrong. However your answer still make sense. – paradigmatic May 17 '13 at 09:35
  • 2
    @paradigmatic I surely agree that Scala supports *path* dependent types, but I am not convinced that these allow you to encode everything that general dependent types let you express. For example, ranged floats. And I am not the only one having doubts, see the comments by sclv to the answer you referenced. – Malte Schwerhoff May 17 '13 at 09:49
  • @paradigmatic path-dependent types are not really what type theorists mean when they say dependent types. The basic point is that you _can't_ encode this ranged float notion using PDTs, and can using DTs. You can "lift" a big chunk of what it means to be a `Float` and reflect it at the type level, but you can do that in languages with much weaker type systems too, so I'm not sure that's a particularly valuable criterion. Much like Miles did in his answer, I can call a Haskell function `f :: SomeGADT a -> a` a "Pi type" and while it does have some features of one, it's not the same thing. – Mysterious Dan May 17 '13 at 14:41
  • I'm not sure why Scala people are so defensive about this topic (outright saying anyone contradicting it is "wrong" without actually understanding the underlying type theory). Nobody's impugning the awesomeness of Scala's type system. Many dependent types people still love doing crazy stuff with Haskell's type level, and much of that is effectively dependent types, but you still aren't actually depending on values. It's cool and powerful, but if you start talking about "limited dependent types" then C++ fits the bill, too, and I don't see the point in confusing the terminology further. – Mysterious Dan May 17 '13 at 14:43
  • Would it have been less controversial had my comment had said "it's worth noting that even if Scala _could_ express this, it would come at a huge cost" without mentioning the loaded DT term? The meaning is the same, because Scala can't express a ranged float type. Agda can. Call the distinction what you want. – Mysterious Dan May 17 '13 at 14:47
  • @paradigmatic I added a paragraph making an intuitive argument why natural numbers can be handled, but reals cannot. – Malte Schwerhoff May 21 '13 at 09:15
  • @mhs thanks for the explanation, I finally understood you point. – paradigmatic May 21 '13 at 19:32
  • @mhs I think the more precise statement is about whether a type is inductive/data or not. Naturals are inductive, and HLists are too; reals are not, and streams aren't either. The fact that we can have reasonably arbitrary type-level functions suggests that we can probably do some coinductive stuff too, though. One encoding of the rationals is as a pair of an integer and a natural, and one encoding of the reals is as a function from the rationals to the rationals. Unfortunately, Scala's type functions mostly need to be written in pseudo-CPS since you can't switch on types directly. – Mysterious Dan May 23 '13 at 17:36
2

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

Beryllium
  • 12,808
  • 10
  • 56
  • 86
2

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.

paradigmatic
  • 40,153
  • 18
  • 88
  • 147