Scala functional programming gymnastics

后端 未结 13 670
悲&欢浪女
悲&欢浪女 2021-02-01 09:36

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 : D         


        
相关标签:
13条回答
  • 2021-02-01 10:32

    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…?

    0 讨论(0)
  • 2021-02-01 10:34

    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) 
    }
    
    0 讨论(0)
  • 2021-02-01 10:35

    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.

    0 讨论(0)
  • 2021-02-01 10:36

    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
    
    0 讨论(0)
  • 2021-02-01 10:40

    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!

    0 讨论(0)
  • 2021-02-01 10:41

    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)
    
    0 讨论(0)
提交回复
热议问题