scala currying/partials для создания списка фильтров функций

Учитывая следующий код:

case class Config(
  addThree: Boolean = true,
  halve: Boolean = true,
  timesFive: Boolean = true
)


def doOps(num: Integer, config: Config): Integer = {
  var result: Integer = num
  if ( config.addThree ) {
    result += 3
  }
  if ( config.halve ) {
    result /= 2
  }
  if ( config.timesFive ) {
    result *= 5
  }
  result
}                                             

val config = Config(true,false,true)          

println( doOps(20, config) )
println( doOps(10, config) )

Я хотел бы заменить уродливый метод doOps более эффективной и идиоматической конструкцией. В частности, я хотел бы построить цепочку функций, которая выполняет только необходимые преобразования на основе конкретной используемой конфигурации. Я знаю, что, вероятно, хочу создать какую-то частично прикладную функцию, в которую я могу передать Integer, но я не знаю, как добиться этого эффективным способом.

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

Результирующий код, я думаю, будет выглядеть примерно так:

case class Config(
  addThree: Boolean = true,
  halve: Boolean = true,
  timesFive: Boolean = true
)

def buildDoOps(config: Config) = ???

val config = Config(true,false,true)
def doOps1 = buildDoOps(config)

println( doOps1(20) )
println( doOps1(10) )

person Connie Dobbs    schedule 15.02.2013    source источник


Ответы (4)


Вот мое предложение. В основном я создаю последовательность функций, независимых друг от друга. Если одна из операций отключена, я заменяю ее на identity. В конце концов я foldLeft перебрал эту последовательность, используя аргумент num в качестве начального значения:

case class Config(
  addThree: Boolean = true,
  halve: Boolean = true,
  timesFive: Boolean = true
) {

  private val funChain = Seq[Int => Int](
    if(addThree) _ + 3 else identity _,
    if(halve) _ / 2 else identity _,
    if(timesFive) _ * 5 else identity _
  )

  def doOps(num: Int) = funChain.foldLeft(num){(acc, f) => f(acc)}

}

Я поместил doOps() внутрь Config, так как он туда прекрасно вписывается.

Config(true, false, true).doOps(10)  //(10 + 3 ) * 5 = 65

Если вы мазохист, foldLeft() можно написать так:

def doOps(num: Int) = (num /: funChain){(acc, f) => f(acc)}

Если вам не нравится identity, используйте Option[Int => Int] и flatten:

private val funChain = Seq[Option[Int => Int]](
    if(addThree) Some(_ + 3) else None,
    if(halve) Some(_ / 2) else None,
    if(timesFive) Some(_ * 5) else None
).flatten
person Tomasz Nurkiewicz    schedule 15.02.2013
comment
Очень интересная конструкция. Это может быть несущественным (в зависимости от того, как scala/jvm в конечном итоге оптимизирует код), но похоже, что результирующий путь выполнения включает в себя выполнение функции identity вместо того, чтобы просто пропустить выполнение (как если бы Seq никогда не включал этот шаг). Я явно хотел избежать оператора if, интересно, как дополнительная рекурсия для identity влияет на фактический путь выполнения. Я предполагаю, что Seq можно было бы сначала отфильтровать, чтобы удалить вызовы identity перед foldLeft, но я не уверен, что это действительно окажется избыточным. - person Connie Dobbs; 15.02.2013
comment
@ConnieDobbs: см. вторую версию funChain с использованием Option[Int => Int] и flatten и избеганием identity. - person Tomasz Nurkiewicz; 15.02.2013
comment
Спасибо, Томас, комбинация Option/flatten, которую вы добавили в своем последнем редактировании, я думаю, позволяет избежать любой (возможной / теоретической) неэффективности дополнительного вызова функции identity, в то время как (IMO) фактически улучшает читаемость. - person Connie Dobbs; 16.02.2013
comment
Одна вещь, которую вы, возможно, захотите добавить к своему ответу, Томас: в случае, когда мы не используем identity, вероятно, стоит добавить последний Some(_) к Seq, чтобы, если все варианты false, это не просто пустой Seq, (который не вернет исходную строку в чистом виде). - person Connie Dobbs; 16.02.2013
comment
@ConnieDobbs: на самом деле foldLeft() над пустой последовательностью безопасно, поэтому, если funChain пусто, возвращается начальное значение (num). Это на самом деле имеет смысл - никаких преобразований, ничего не делать. - person Tomasz Nurkiewicz; 16.02.2013
comment
@Connie Dobs: если скорость важна в вашем случае использования, имейте в виду, что в приведенном выше решении мы выполняем foldLeft каждый раз, когда мы могли бы сделать это только один раз, чтобы построить составную функцию (см. мой ответ для примера), которая конечно быстрее. - person Régis Jean-Gilles; 16.02.2013

Подобно решению Томаша Нуркевича, но с использованием моноида Скалаз для эндоморфизмов (функции, которые имеют одинаковый входной и выходной тип).

Операция добавления моноида — compose, а элемент идентификации — функция identity.

import scalaz._, Scalaz._

def endo(c: Config): Endo[Int] =
  c.timesFive ?? Endo[Int](_ * 5) |+|
  c.halve ?? Endo[Int](_ / 2) |+|
  c.addThree ?? Endo[Int](_ + 3)

def doOps(n: Int, c: Config) = endo(c)(n)

Оператор ?? возвращает правый операнд, когда левый операнд равен true, и элемент идентификации моноида, когда false.

Обратите внимание, что порядок составления функций обратный порядку их применения.

person Ben James    schedule 15.02.2013

Вы можете просто добавить дополнительные функции в класс case Config, как показано ниже. Это позволит вам связать вызовы функций вместе, как вы упомянули.

case class Config(
  doAddThree : Boolean = true,
  doHalve : Boolean = true,
  doTimesFive : Boolean = true
) {
  def addThree(num : Integer) : Integer = if(doAddThree) (num+3) else num
  def halve(num : Integer) : Integer = if(doHalve) (num/2) else num
  def timesFive(num : Integer) : Integer = if(doTimesFive) (num*5) else num
}


def doOps(num: Integer, config: Config): Integer = {
  var result: Integer = num
  result = config.addThree(result)
  result = config.halve(result)
  result = config.timesFive(result)
  result
}                                             

val config = Config(true,false,true)          

def doOps1(num : Integer) = doOps(num, config)

println( doOps1(20) )
println( doOps1(10) )

Более чистый способ сделать эту «цепочку» - использовать foldLeft в списке частично примененных функций, подобно тому, что упоминается в одном из других ответов:

def doOps(num: Integer, config: Config): Integer = {
  List(
    config.addThree(_),
    config.halve(_),
    config.timesFive(_)
  ).foldLeft(num) {
    case(x,f) => f(x)
  }
}
person 808sound    schedule 15.02.2013

Если вы хотите перейти к более декларативному (и расширяемому) стилю, вы можете сделать это:

import collection.mutable.Buffer

abstract class Config {
  protected def Op( func: Int => Int )( enabled: Boolean) {
    if ( enabled ) {
      _ops += func
    }   
  }
  private lazy val _ops = Buffer[Int => Int]()
  def ops: Seq[Int => Int] = _ops
}

def buildDoOps(config: Config): Int => Int = {
  val funcs = config.ops
  if ( funcs.isEmpty ) identity // Special case so that we don't compose with identity everytime
  else funcs.reverse.reduceLeft(_ andThen _)
}

Теперь вы можете просто определить свою конфигурацию следующим образом:

case class MyConfig(
  addThree: Boolean = true,
  halve: Boolean = true,
  timesFive: Boolean = true
) extends Config {
  Op(_ + 3)(addThree)
  Op(_ / 3)(halve)
  Op(_ * 5)(timesFive)
}

И напоследок немного теста в REPL:

scala> val config = new MyConfig(true,false,true)
config: MyConfig = MyConfig(true,false,true)
scala> val doOps1 = buildDoOps(config)
doOps1: Int => Int = <function1>
scala> println( doOps1(20) )
115
scala> println( doOps1(10) )
65    

Обратите внимание, что buildDoOps принимает экземпляр Config, который является абстрактным. Другими словами, он работает с любым подклассом Config (например, MyConfig выше), и вам не нужно будет переписывать его при создании другого типа конфигурации.

Кроме того, buildDoOps возвращает функцию, которая выполняет только запрошенные операции, что означает, что мы не проверяем без необходимости значения в конфигурации каждый раз, когда применяем функцию (но только при ее создании). На самом деле, учитывая, что функция зависит только от состояния конфигурации, мы могли бы (и, вероятно, должны) просто определить для нее lazy val прямо в Config (это значение result ниже):

abstract class Config {
  protected def Op( func: Int => Int )( enabled: Boolean) {
    if ( enabled ) {
      _ops += func
    }   
  }
  private lazy val _ops = Buffer[Int => Int]()
  def ops: Seq[Int => Int] = _ops
  lazy val result: Int => Int = {
    if ( ops.isEmpty ) identity // Special case so that we don't compose with identity everytime
    else ops.reverse.reduceLeft(_ andThen _)
  }
}    

Тогда мы бы сделали:

case class MyConfig(
  addThree: Boolean = true,
  halve: Boolean = true,
  timesFive: Boolean = true
) extends Config {
  Op(_ + 3)(addThree)
  Op(_ / 3)(halve)
  Op(_ * 5)(timesFive)
}

val config = new MyConfig(true,false,true)
println( config.result(20) )
println( config.result(10) )
person Régis Jean-Gilles    schedule 15.02.2013