Подтвердить что ты не робот

Clojure 'let' эквивалент в Scala

Часто я сталкиваюсь с следующей ситуацией: предположим, что у меня есть эти три функции

def firstFn: Int = ...
def secondFn(b: Int): Long = ...
def thirdFn(x: Int, y: Long, z: Long): Long = ...

а также функция calculate. Мой первый подход может выглядеть так:

def calculate(a: Long) = thirdFn(firstFn, secondFn(firstFn), secondFn(firstFn) + a)

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

def calculate(a: Long) = {
  val first = firstFn
  val second = secondFn(first)

  thirdFn(first, second, second + a)
}

Теперь это несколько выражений, окруженных фигурными скобками. В такие моменты я немного завидую Clojure. С let function я могу определить эту функцию в одном выражении.

Итак, моя цель - определить функцию calculate с одним выражением. Я придумал 2 решения.

1 - С scalaz Я могу определить его как это (есть ли лучшие способы сделать это с помощью scalaz?):

  def calculate(a: Long) = 
    firstFn |> {first => secondFn(first) |> {second => thirdFn(first, second, second + a)}}

Что мне не нравится в этом решении, так это то, что он вложен. Чем больше val, тем глубже это гнездование.

2 - С пониманием for я могу добиться чего-то подобного:

  def calculate(a: Long) = 
    for (first <- Option(firstFn); second <- Option(secondFn(first))) yield thirdFn(first, second, second + a)

С одной стороны, это решение имеет плоскую структуру, как и let в Clojure, но с другой стороны мне нужно обернуть результаты функций в Option и получить Option в результате calculate ( это хорошо, что я имею дело с нулями, но я не... и не хочу).

Есть ли лучшие способы достижения моей цели? Каков идиоматический способ решения таких ситуаций (может быть, я должен оставаться с val s... но let способ сделать это выглядит так элегантно)?

С другой стороны, он связан с ссылочной прозрачностью. Все три функции являются ссылочно прозрачными (в моем примере firstFn вычисляется некоторая константа как Pi), поэтому теоретически их можно заменить результатами расчета. Я знаю это, но компилятор этого не делает, поэтому он не может оптимизировать мою первую попытку. И вот мой второй вопрос:

Могу ли я каким-либо образом (может быть, с аннотацией) дать подсказку компилятору, что моя функция является ссылочной прозрачностью, так что она может оптимизировать эту функцию для меня (например, класть туда какое-то кеширование)?

Изменить

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

4b9b3361

Ответ 1

в нерекурсивном случае, это реструктуризация лямбда.

def firstFn : Int = 42
def secondFn(b : Int) : Long = 42
def thirdFn(x : Int, y : Long, z : Long) : Long = x + y + z

def let[A, B](x : A)(f : A => B) : B = f(x)

def calculate(a: Long) = let(firstFn){first => let(secondFn(first)){second => thirdFn(first, second, second + a)}}

Конечно, это все еще вложенное. Не могу этого избежать. Но ты сказал, что тебе нравится монадическая форма. Итак, здесь монотонная монада

case class Identity[A](x : A) {
   def map[B](f : A => B) = Identity(f(x))
   def flatMap[B](f : A => Identity[B]) = f(x)
}

И вот ваш монадический расчет. Разверните результат, вызвав .x

def calculateMonad(a : Long) = for {
   first <- Identity(firstFn)
   second <- Identity(secondFn(first))
} yield thirdFn(first, second, second + a)

Но на данный момент это похоже на исходную версию val.

Монастырь Identity существует в Scalaz с большей изощренностью

http://scalaz.googlecode.com/svn/continuous/latest/browse.sxr/scalaz/Identity.scala.html

Ответ 2

Придерживайтесь оригинальной формы:

def calculate(a: Long) = {
  val first = firstFn
  val second = secondFn(first)

  thirdFn(first, second, second + a)
}

Это красноречиво и понятно, даже разработчикам Java. Это примерно эквивалентно let, просто не ограничивая объем имен.

Ответ 3

Здесь вы можете упустить вариант.

def calculate(a: Long)(i: Int = firstFn)(j: Long = secondFn(i)) = thirdFn(i,j,j+a)

Если вы действительно хотите создать метод, я бы это сделал.

В качестве альтернативы вы можете создать метод (можно назвать его let), который позволяет избежать вложенности:

class Usable[A](a: A) {
  def use[B](f: A=>B) = f(a)
  def reuse[B,C](f: A=>B)(g: (A,B)=>C) = g(a,f(a))
  // Could add more
}
implicit def use_anything[A](a: A) = new Usable(a)

def calculate(a: Long) =
  firstFn.reuse(secondFn)((first, second) => thirdFn(first,second,second+a))

Но теперь вам может понадобиться много раз называть одни и те же вещи.

Ответ 4

Если вы чувствуете, что первая форма чище/элегантнее/читабельнее, то почему бы просто не придерживаться ее?

Сначала прочитайте это последнее сообщение о фиксации компилятору Scala от кого-то, кроме Мартина Одерского, и примите его к сердцу...


Возможно, настоящая проблема здесь мгновенно прыгает с пистолета, утверждая, что она не оптимальна. JVM довольно жарко в оптимизации такого рода вещей. Время от времени это просто потрясающе!

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

Затем, и только тогда, вы должны взглянуть на способы сделать это быстрее, что может в конечном итоге пожертвовать ясностью кода.

Ответ 5

Почему бы не использовать сопоставление шаблонов здесь:

def calculate (a: Long) = firstFn match {case f = > secondFn (f) match {case s = > thirdFn (f, s, s + a)}}

Ответ 6

Как насчет использования currying для записи возвращаемых значений функции (параметры из предыдущих групп параметров доступны в нескольких группах).

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

def calculate(a: Long)(f: Int = firstFn)(s: Long = secondFn(f)) = thirdFn(f, s, s + a)

println(calculate(1L)()())