Scala функциональное программирование гимнастика - PullRequest
10 голосов
/ 10 ноября 2010

Я пытаюсь сделать следующее, используя как можно меньше кода и как можно более функционально:

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

Очевидно следующие работы:

= (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
  }

Я действительно надеялся на что-то более элегантное и приму использование библиотеки Scalaz ! Можно предположить, что верно следующее :

floor.forall( f => cap.forall( _ > f))
<Ч />

Если кому-то интересно, вот тестовый код :

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

Ответы [ 13 ]

16 голосов
/ 10 ноября 2010

Редактировать 2 :

Размышляя о методе cataX, я понял, что cataX - это не что иное, как простая и простая складка. Используя это, мы можем получить чистое решение scala без каких-либо дополнительных библиотек.

Итак, вот оно:

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

, что совпадает с

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

(не то чтобы это обязательно легче понять).

Я думаю, вы не можете иметь его короче.


К лучшему или худшему, мы также можем решить эту проблему с помощью скаляза:

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

или даже:

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

Будучи «нормальным» программистом Scala, вы можете не знать о специальных операторах и методах Scalaz (|> и Option.cata). Они работают следующим образом:

value |> function переводится в function(value) и, таким образом, amt |> (m => v fun m) равно v fun amt.

opt.cata(fun, v) переводится как

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

или opt.map(fun).getOrElse(v).

См. Определения Скалаза для cata и |>.

Более симметричное решение было бы:

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

Редактировать: Извините, сейчас становится странно, но я также хотел иметь версию без очков. Новый cataX карри. Первый параметр принимает двоичную функцию; второе значение.

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)

Если o соответствует Some, мы возвращаем функцию со значением o и примененным вторым параметром, если o соответствует None, мы возвращаем только второй параметр.

А вот и мы:

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

Может быть, у них это уже есть в Скалазе? ...

15 голосов
/ 10 ноября 2010

Не так лаконично, как скалярная версия, но, с другой стороны, никаких зависимостей,

List(floor.getOrElse(Double.NegativeInfinity), cap.getOrElse(Double.PositiveInfinity), amt).sorted.apply(1)
5 голосов
/ 12 ноября 2010

Вместо краткости, это показывает, насколько проще становится композиция, если вы превращаете cap и floor в функции.

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)

Из скалаза я использую MA#∘, функторкарта, как способ использования Option.map и Function1.andThenOptionW#|, который является псевдонимом для Option.getOrElse.

ОБНОВЛЕНИЕ

Это то, что я искал:

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] - это обертка вокруг A => A, есть неявные преобразования в обоих направлениях.Существует экземпляр Monoid, определенный для Endo[A], Monoid#plus объединяет функции, а Monoid#zero возвращает функцию идентификации.Если у нас есть List из Endo[A], мы можем суммировать список и получить единственное значение, которое можно использовать как A => A.

MA#foldMap отображает данную функцию через Foldable тип данных, и сводится к одному значению с Monoid.foldMapEndo удобство в дополнение к этому.Эта абстракция позволяет вам легко перейти от проверки крышки и пола за Option с к любому складному типу, например List.

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

Еще один рефакторинг может привести к:

val capAndFloor = Seq((cap, min), (floor, max)).foldMap { case (a, f) => foldMapEndo(a, f) }
capAndFloor: scalaz.Endo[Int] = scalaz.Endos$$anon$1@25b85c8e
5 голосов
/ 10 ноября 2010

Начну с этого:

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

Но у меня такое чувство, что я упускаю какую-то возможность здесь, поэтому я не могу закончить.

4 голосов
/ 10 ноября 2010

Не красивее, не намного короче и, конечно, не быстрее! Но он более сложный, более общий и более «функциональный»:

РЕДАКТИРОВАТЬ : сделал код полностью универсальным:)

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)

ОБНОВЛЕНИЕ 2 : есть и эта версия, использующая лучшее преимущество 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)

Надеюсь, вам нравятся подписи типа:)

ОБНОВЛЕНИЕ 3 : Вот тот, который делает то же самое с границы

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

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

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

ОБНОВЛЕНИЕ 4 : Переверните раствор вверх дном. Этот предлагает более интересные возможности для будущего расширения.

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 

Если мне повезет, я вдохновлю кого-то другого создать лучшее решение из моего. Вот как обычно это получается ...

4 голосов
/ 10 ноября 2010

Как насчет этого?

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

[Изменить]

Вторая попытка:

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

Выглядит немного слишком "Лиспи"на мой вкус, но проходит тесты: -)

[2nd Edit]

Первая версия тоже может быть" отремонтирована ":

def restrict(floor: Option[Double], cap: Option[Double], amt: Double): Double =
  (floor.getOrElse(-Double.MaxValue) max amt) min cap.getOrElse(Double.MaxValue)
2 голосов
/ 14 ноября 2010

Это на основе ответа Кена Блума :

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
}

В итоге получается код, подобный следующему:

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

Я думаю, это довольно круто ине страдает от проблемы с решением Кена (на мой взгляд), который заключается в том, что это хак !

2 голосов
/ 14 ноября 2010

Я обнаружил, что когда вопрос просит использовать Option для указания необязательного параметра, обычно существует более естественный способ представления отсутствующего параметра. Поэтому я собираюсь немного изменить интерфейс и использовать аргументы по умолчанию для определения функции и именованных параметров для вызова функции.

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

Тогда вы можете позвонить:

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

( Еще один пример того же принципа. )

2 голосов
/ 10 ноября 2010

В Scalaz это не намного проще, чем в обычной 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

(Добавьте _ после max и min, если вам будет легче увидеть, куда идет параметр.)

Scalaz немного легче читать, когда вы понимаете, что делают операторы.

1 голос
/ 14 ноября 2010

Это еще один способ исправить Первый ответ Ландеи

def restrict(floor : Option[Double], cap : Option[Double], amt : Double) : Double = {
  val chopBottom = (floor.getOrElse(amt) max amt) 
  chopBottom min cap.getOrElse(chopBottom)
}
...