10

I am trying to do the following in as little code as possible and as functionally as possible:

def restrict(floor : Option[Double], cap : Option[Double], amt : Double) : Double

Obviously the following works:

= (floor -> cap) match {
    case (None, None)       => amt
    case (Some(f), None)    => f max amt 
    case (None, Some(c))     => c min amt
    case (Some(f), Some(c)) => (f max amt) min c
  }

I was really hoping for something more elegant and will accept use of the Scalaz library! You can assume that the following is true:

floor.forall( f => cap.forall( _ > f))

If anyone is interested, here is some test code:

object Comparisons {
  sealed trait Cf {
    def restrict(floor: Option[Double], cap: Option[Double], amt: Double): Double
  }

  def main(args: Array[String]) {
    val cf : Cf = //TODO - your impl here!
    def runtest(floor: Option[Double], cap: Option[Double], amt: Double, exp : Double) : Unit = {
      val ans = cf.restrict(floor, cap, amt)
      println("floor=%s, cap=%s, amt=%s === %s (%s) : %s".format(floor, cap, amt, ans, exp, if (ans == exp) "PASSED" else "FAILED"))
    }
    runtest(Some(3), Some(5), 2, 3)
    runtest(Some(3), Some(5), 3, 3)
    runtest(Some(3), Some(5), 4, 4)
    runtest(Some(3), Some(5), 5, 5)
    runtest(Some(3), Some(5), 6, 5)

    runtest(Some(3), None, 2, 3)
    runtest(Some(3), None, 3, 3)
    runtest(Some(3), None, 4, 4)
    runtest(Some(3), None, 5, 5)
    runtest(Some(3), None, 6, 6)

    runtest(None, Some(5), 2, 2)
    runtest(None, Some(5), 3, 3)
    runtest(None, Some(5), 4, 4)
    runtest(None, Some(5), 5, 5)
    runtest(None, Some(5), 6, 5)

    runtest(None, None, 2, 2)
    runtest(None, None, 3, 3)
    runtest(None, None, 4, 4)
    runtest(None, None, 5, 5)
    runtest(None, None, 6, 6)
  }
}
oxbow_lakes
  • 133,303
  • 56
  • 317
  • 449
  • 1
    What does the "restrict" function suppose to do? – OscarRyz Nov 10 '10 at 15:22
  • Sorry - it has to return a Double based on the supplied `amt` but which is capped or floored by the optional parameters. That is, if supplied 10 but a cap of Some(8), the method should return 8 – oxbow_lakes Nov 10 '10 at 18:07

13 Answers13

16

Edit 2:

While thinking about the cataX method, I figured out that cataX is nothing else than a plain and simple fold. Using that, we can get a pure scala solution without any additional libraries.

So, here it is:

( (amt /: floor)(_ max _) /: cap)(_ min _)

which is the same as

cap.foldLeft( floor.foldLeft(amt)(_ max _) )(_ min _)

(not that this is necessarily easier to understand).

I think you can’t have it any shorter than that.


For better or worse, we can also solve it using scalaz:

floor.map(amt max).getOrElse(amt) |> (m => cap.map(m min).getOrElse(m))

or even:

floor.cata(amt max, amt) |> (m => cap.cata(m min, m))

As a ‘normal’ Scala programmer, one might not know about the special Scalaz operators and methods used (|> and Option.cata). They work as follows:

value |> function translates to function(value) and thus amt |> (m => v fun m) is equal to v fun amt.

opt.cata(fun, v) translates to

opt match {
  case Some(value) => fun(value) 
  case None => v
}

or opt.map(fun).getOrElse(v).

See the Scalaz definitions for cata and |>.

A more symmetric solution would be:

amt |> (m => floor.cata(m max, m)) |> (m => cap.cata(m min, m))

Edit: Sorry, it’s getting weird now, but I wanted to have a point-free version as well. The new cataX is curried. The first parameter takes a binary function; the second is a value.

class CataOption[T](o: Option[T]) {
  def cataX(fun: ((T, T) => T))(v: T) = o.cata(m => fun(m, v), v)
}
implicit def option2CataOption[T](o: Option[T]) = new CataOption[T](o)

If o matches Some we return the function with the value of o and the second parameter applied, if o matches None we only return the second parameter.

And here we go:

amt |> floor.cataX(_ max _) |> cap.cataX(_ min _)

Maybe they already have this in Scalaz…?

Debilski
  • 66,976
  • 12
  • 110
  • 133
  • 2
    You deserve all the points - the middle answer is full of awesomeness. Might be an idea to elucidate what is going on – oxbow_lakes Nov 10 '10 at 15:20
  • 1
    The highly anticipated scalaz elucidation library is still very much a work in progress. As a sneak preview though... I believe that for the most part it's based on a little-known mathematical proof demonstrating a homomorphism between source code and clear descriptions :) – Kevin Wright Nov 10 '10 at 15:38
  • The thing is, once you understand the type signature of `|>`, it's obvious, innit? – oxbow_lakes Nov 10 '10 at 15:50
  • `|>` is pretty nice. I’d use it more often, if I knew, I would be understood. – Debilski Nov 10 '10 at 16:16
  • @Kevin, Numeric.min gives me some problems with implicits and scalaz.Order but maybe I’m just not doing it right. It’s not that important, of course. – Debilski Nov 10 '10 at 19:25
  • 2
    @Debilski FWIW, `|>` is one of the most important F# operators. – Daniel C. Sobral Nov 10 '10 at 23:32
  • @Daniel: I know; I’d have mentioned it but I wasn’t sure if it was invented for F# or if it has some background in other functional programming languages as well. – Debilski Nov 11 '10 at 02:43
  • 1
    Okay, I’m down to 37 characters pure scala (no scalaz) using `/:`. – Debilski Nov 12 '10 at 13:41
15

Not quite as terse as the scalaz version, but on the other hand, no dependencies,

List(floor.getOrElse(Double.NegativeInfinity), cap.getOrElse(Double.PositiveInfinity), amt).sorted.apply(1)
Miles Sabin
  • 23,015
  • 6
  • 61
  • 95
  • I love this one. All the other solutions so far force you to understand what two tests do, where as this solution picks the middle between floor and cap. – huynhjl Nov 11 '10 at 03:21
5

I'll start with this:

def restrict(floor : Option[Double], cap : Option[Double], amt : Double) : Double = {
  val flooring = floor.map(f => (_: Double) max f).getOrElse(identity[Double] _)       
  val capping  = cap.map(f => (_: Double) min f).getOrElse(identity[Double] _)         
  (flooring andThen capping)(amt)                                                      
}                                                                                    

But I have the feeling I'm missing some opportunity here, so I may not be finished.

Daniel C. Sobral
  • 295,120
  • 86
  • 501
  • 681
5

Rather than going for pure brevity, this shows how much easier composition becomes if you turn cap and floor into functions.

scala> val min = (scala.math.min _).curried                                        
min: (Int) => (Int) => Int = <function1>

scala> val max = (scala.math.max _).curried                                        
max: (Int) => (Int) => Int = <function1>

scala> def orIdentity[A](a: Option[A])(f: A => A => A): (A => A) = a ∘ f | identity
orIdentity: [A](a: Option[A])(f: (A) => (A) => A)(A) => A

scala> val cap = 5.some; val floor = 1.some                                        
cap: Option[Int] = Some(5)
floor: Option[Int] = Some(1)

scala> val ffloor = orIdentity(floor)(max)                                         
ffloor: (Int) => Int = <function1>

scala> val fcap = orIdentity(cap)(min)                                             
fcap: (Int) => Int = <function1>

scala> val capAndFloor = fcap ∘ ffloor                                             
capAndFloor: (Int) => Int = <function1>    

scala> (0 to 8).toSeq ∘ (capAndFloor)    
res0: Seq[Int] = Vector(1, 1, 2, 3, 4, 5, 5, 5, 5)

From scalaz, I use MA#∘, the functor map, both as a way of using Option.map and Function1.andThen; and OptionW#| which is an alias for Option.getOrElse.

UPDATE

This is what I was looking for:

scala> import scalaz._; import Scalaz._
import scalaz._
import Scalaz._

scala> val min = (scala.math.min _).curried                                        
min: (Int) => (Int) => Int = <function1>

scala> val max = (scala.math.max _).curried                                        
max: (Int) => (Int) => Int = <function1>

scala> def foldMapEndo[F[_]: Foldable, A](fa: F[A], f: A => A => A): Endo[A] = 
     |    fa.foldMap[Endo[A]](a => f(a))                                       
foldMapEndo: [F[_],A](fa: F[A],f: (A) => (A) => A)(implicit evidence$1: scalaz.Foldable[F])scalaz.Endo[A]

scala> val cap = 5.some; val floor = 1.some                                    
cap: Option[Int] = Some(5)
floor: Option[Int] = Some(1)    

scala> val capAndFloor = List(foldMapEndo(floor, max), foldMapEndo(cap, min)) ∑
capAndFloor: scalaz.Endo[Int] = scalaz.Endos$$anon$1@4352d1fc

scala>(0 to 10).toSeq.map(capAndFloor)                                               
res0: Seq[Int] = Vector(1, 1, 2, 3, 4, 5, 5, 5, 5, 5, 5)

scalaz.Endo[A] is a wrapper around A => A, there are implicit conversions in both directions. There is an instance of Monoid defined for Endo[A], Monoid#plus chains the functions, and Monoid#zero returns the identity function. If we have a List of Endo[A], we can sum the list and result in a single value, which can be used as A => A.

MA#foldMap maps the given function over a Foldable data type, and reduces to a single value with a Monoid. foldMapEndo is a convenience on top of this. This abstraction allows you to easily change from proving the cap and floor in Options to any foldable type, such as a List.

val capAndFloor = Seq(foldMapEndo(List(1, 2, max), foldMapEndo(cap, min)).collapsee
capAndFloor: scalaz.Endo[Int] = scalaz.Endos$$anon$1@76f40c39

Another refactoring might lead to:

val capAndFloor = Seq((cap, min), (floor, max)).foldMap { case (a, f) => foldMapEndo(a, f) }
capAndFloor: scalaz.Endo[Int] = scalaz.Endos$$anon$1@25b85c8e
retronym
  • 54,768
  • 12
  • 155
  • 168
  • 1
    Awesome as always! This approach is obviously useful if I wanted to compose cap/floor with other functions, or in other places in the codebase - it's actually in one small place – oxbow_lakes Nov 11 '10 at 22:13
  • The approach is quite similar to Kevin's and Daniel's answers. It's interesting to see how things get easier to plumb together with curried functions. I'm still wondering if there is an abstraction lurking behind `orIdentity`. – retronym Nov 11 '10 at 22:17
  • I take it that you could have used `|+|` instead of the `List.sum` approach? – oxbow_lakes Nov 11 '10 at 23:41
  • Absolutely, both use `Monoid[Endo[Int]]`, – retronym Nov 12 '10 at 06:20
4

How about this?

//WRONG
def restrict(floor : Option[Double], cap : Option[Double], amt : Double) : Double = 
   (floor.getOrElse(amt) max amt) min cap.getOrElse(amt)

[Edit]

Second try:

def restrict(floor : Option[Double], cap : Option[Double], amt : Double) : Double = 
   floor.map(f => f max _).getOrElse(identity[Double] _)(
     cap.map(c => c min _).getOrElse(identity[Double] _)(amt))

Looks a little bit too "lispy" for my taste, but passes the tests :-)

[2nd Edit]

The first version can be "repaired", too:

def restrict(floor: Option[Double], cap: Option[Double], amt: Double): Double =
  (floor.getOrElse(-Double.MaxValue) max amt) min cap.getOrElse(Double.MaxValue)
Landei
  • 54,104
  • 13
  • 100
  • 195
4

Not prettier, not much shorter, and certainly not faster! But it is more composable more generic and more "functional":

EDIT: made the code fully generic :)

def optWith[T](a: Option[T], b: T)(op:(T,T)=>T) =
  a map (op(b,_)) getOrElse b

def optMin[T:Numeric](a: Option[T]) =
  (b:T) => optWith(a, b)(implicitly[Numeric[T]].min)

def optMax[T:Numeric](a: Option[T]) =
  (b:T) => optWith(a, b)(implicitly[Numeric[T]].max)

def restrict[T,FT,CT](x:T, floor:Option[FT], ceil:Option[CT])
  (implicit ev:Numeric[T], fv:FT=>T, cv:CT=>T) =
  optMin(ceil map cv) compose optMax(floor map fv) apply(x)

UPDATE 2: There's also this version, taking better advantage of Numeric

def optWith[T](a: Option[T])(op:(T,T)=>T) =
  (b:T) => a map (op(b,_)) getOrElse b

def restrict[T,FT,CT](x:T, floor:Option[FT], ceil:Option[CT])
  (implicit n:Numeric[T], fv:FT=>T, cv:CT=>T) =
  optWith(ceil map cv)(n.min) compose optWith(floor map fv)(n.max) apply(x)

I hope you like type signatures :)

UPDATE 3: Here's one that does the same with bounds

def optWith[T, V <% T](op:(T,T)=>T)(a: Option[V]) =
  (b:T) => a map (op(b,_)) getOrElse b

def restrict[T:Numeric, FT <% T, CT <% T]
(floor:Option[FT], ceil:Option[CT], amt:T) = {
  val n = implicitly[Numeric[T]]; import n._
  optWith(min)(ceil) compose
  optWith(max)(floor) apply(amt)
}

If nothing else... this shows quite clearly why import parameters would be a Good Thing(tm). Imagine if the following was valid code:

def optWith[T, V <% T](op:(T,T)=>T)(a: Option[V]) =
  (b:T) => a map (op(b,_)) getOrElse b

def restrict[import T:Numeric,FT <% T,CT <% T]
(floor:Option[FT], ceil:Option[CT], amt:T) = {
  optWith(min)(ceil) compose
  optWith(max)(floor) apply(amt)
}

UPDATE 4: Turning the solution upside down here. This one offers some more interesting possibilities for future extension.

implicit def optRhs[T:Ordering](lhs:T) = new Object {
  val ord = implicitly[Ordering[T]]; import ord._

  private def opt(op: (T,T)=>T)(rhs:Option[T]) =
    rhs map (op(lhs,_)) getOrElse lhs

  def max = opt(ord.max) _
  def min = opt(ord.min) _
}

def restrict[T : Ordering](floor:Option[T], cap:Option[T], amt:T) =
  amt min cap max floor 

With any luck, I'll inspire someone else to build a better solution out of mine. That's how these things usually work out...

Kevin Wright
  • 49,540
  • 9
  • 105
  • 155
  • Great solution in update 4. Quite ridiculous how many different ways of solving this problem there can be in one language. This question is just not going to get asked about Java! – oxbow_lakes Nov 14 '10 at 19:41
  • True... In Java you'd just make explicit null checks, possibly risk NPEs, and have to overload N times for every primitive you might end up working with. Even then, it still wouldn't be valid code for any other ordered type you might care about... – Kevin Wright Nov 14 '10 at 22:19
2

This isn't really much easier in Scalaz than in regular Scala:

def restrict(floor: Option[Double], cap: Option[Double], amt: Double) =
  floor.map(amt max).orElse(Some(amt)).map(x => cap.map(x min).getOrElse(x)).get

(Add _ after max and min if it makes you feel better to see where the parameter goes.)

Scalaz is a little easier to read, though, once you understand what the operators do.

Rex Kerr
  • 166,841
  • 26
  • 322
  • 407
2

I find that when a question asks to use an Option to indicate an optional parameter, there's usually a more natural way to represent the missing parameter. So I'm going to change the interface a little here, and use default arguments to define the function and named parameters to call the function.

def restrict(amt:Double,
            floor:Double = Double.NegativeInfinity,
            cap:Double=Double.PositiveInfinity):Double =
    (amt min cap) max floor

Then you can call:

restrict(6)
restrict(6, floor = 7)
restrict(6, cap = 5)

(Another example of the same principle.)

Community
  • 1
  • 1
Ken Bloom
  • 57,498
  • 14
  • 111
  • 168
  • I'm not sure I agree with you here; how is using a "trick" more natural than expressing the "truth" of the situation? That said, the call-site syntax is nice! I'm going to add an answer based around your solution which I think is more appropriate – oxbow_lakes Nov 14 '10 at 18:11
  • @oxbow_lakes, How is using infinity to mean "no limit" a trick? How is that not the truth? (I figured that if you were going to object, it would be an objection to the change of interface, not the use of infinity as the limit.) – Ken Bloom Nov 14 '10 at 21:52
  • I see your point; but there is a difference (at least in my mind) between "no limit" and "a limit of infinity". Let's face it, "Double.PositiveInfinity" is a hack! – oxbow_lakes Nov 14 '10 at 22:14
  • I meant it was a hack before your answer! – oxbow_lakes Nov 14 '10 at 22:15
  • @oxbow_lakes: The point of my answer is that overuse of `Option` is an example of bad functional-programming design. Good design dictates that one should look for a way to express "missingness" that fits naturally into the domain of the problem (it becomes easier if you accustom yourself to seeing that as the "truth" of the situation), and only resort to `Option` when it's clear that nothing of the sort exists. – Ken Bloom Nov 14 '10 at 23:52
2

This is based on Ken Bloom's answer:

sealed trait Constrainer { def constrain(d : Double) : Double }

trait Cap extends Constrainer
trait Floor extends Constrainer
case object NoCap extends Cap { def constrain(d : Double) = d }
case object NoFloor extends Floor { def constrain(d : Double) = d }
implicit def d2cap(d : Double) = new Cap { def constrain(amt : Double) = d min amt }
implicit def d2floor(d : Double) = new Floor { def constrain(amt : Double) = d max amt }

def restrict(amt : Double, cap : Cap = NoCap, floor: Floor = NoFloor) : Double = {
  cap.constrain(floor.constrain(amt))
  //or (cap.constrain andThen floor.constrain) amt
}

It ends up with writing code like this:

restrict(amt, cap = 5D)
restrict(amt, floor = 0D)

I think that's pretty awesome and doesn't suffer from the problem with Ken's solution (in my opinion), which is that it is a hack!

Community
  • 1
  • 1
oxbow_lakes
  • 133,303
  • 56
  • 317
  • 449
  • Yes. I think sealed hierarchies and singleton objects are somewhat overlooked. They don't usually make much sense for libraries, but they often show up nicely on business rules. – Daniel C. Sobral Nov 17 '10 at 01:40
1

This is another way to fix Landei's first answer

def restrict(floor : Option[Double], cap : Option[Double], amt : Double) : Double = {
  val chopBottom = (floor.getOrElse(amt) max amt) 
  chopBottom min cap.getOrElse(chopBottom)
}
Community
  • 1
  • 1
Ken Bloom
  • 57,498
  • 14
  • 111
  • 168
0

I'm adding another answer which was inspired by both retronym and Debilski - basically it amounts to converting the cap and floor to functions (Double => Double, if they are present) and then folding the identity function through them with composition:

def restrict(floor: Option[Double], cap: Option[Double], amt: Double) = {
  (identity[Double] _ /: List(floor.map(f => (_: Double) max f), cap.map(c => (_: Double) min c)).flatten){ _ andThen _ }(amt)
}
oxbow_lakes
  • 133,303
  • 56
  • 317
  • 449
0

Straightforward solution with plain Scala and anonymous lambda, without any mappings, folds, Double.{Min/Max}Value, and so on:

def restrict(floor : Option[Double], cap : Option[Double], amt : Double) : Double =
  ((x:Double) => x min cap.getOrElse(x))(amt max floor.getOrElse(amt))
0

I like the initial solution with the match-case most - beside the fact, that I didn't understand that amt means amount (in Germany, 'amt' means 'office') and I only knew cap as something I wear on my head ...

Now here is a really uninspired solution, using an inner method:

def restrict(floor : Option[Double], cap : Option[Double], amt : Double) : Double = {
  def restrict (floor: Double, cap: Double, amt: Double) =
    (floor max amt) min cap
  var f = floor.getOrElse (amt)            
  val c = cap.getOrElse (amt)   
  restrict (f, c, amt) 
}
user unknown
  • 35,537
  • 11
  • 75
  • 121