Как я могу реализовать ранний возврат из-за пределов тела метода в Scala?

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


Итак: предположим, что я нахожусь в довольно длинной функции с множеством последовательных проверок в начале, которые, если они терпят неудачу, должны заставить функцию возвращать какое-то другое значение (не бросать), а в противном случае возвращать нормальное значение . Я не могу использовать return в теле Function. Но могу ли я смоделировать это? Немного похоже на то, что break моделируется в scala.util.control.Breaks?

Я придумал это:

object TestMain {

  case class EarlyReturnThrowable[T](val thrower: EarlyReturn[T], val value: T) extends ControlThrowable
  class EarlyReturn[T] {
    def earlyReturn(value: T): Nothing = throw new EarlyReturnThrowable[T](this, value)
  }

  def withEarlyReturn[U](work: EarlyReturn[U] => U): U = {
    val myThrower = new EarlyReturn[U]
    try work(myThrower)
    catch {
      case EarlyReturnThrowable(`myThrower`, value) => value.asInstanceOf[U]
    }
  }

  def main(args: Array[String]) {
    val g = withEarlyReturn[Int] { block =>
      if (!someCondition)
        block.earlyReturn(4)

      val foo = precomputeSomething
      if (!someOtherCondition(foo))
        block.earlyReturn(5)

      val bar = normalize(foo)
      if (!checkBar(bar))
        block.earlyReturn(6)

      val baz = bazify(bar)
      if (!baz.isOK)
        block.earlyReturn(7)

      // now the actual, interesting part of the computation happens here
      // and I would like to keep it non-nested as it is here
      foo + bar + baz + 42 // just a dummy here, but in practice this is longer
    }
    println(g)
  }
}

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

if (!someCondition) 4 else {
  val foo = precomputeSomething
  if (!someOtherCondition(foo)) 5 else {
    val bar = normalize(foo)
    if (!checkBar(bar)) 6 else {
      val baz = bazify(bar)
      if (!baz.isOK) 7 else {
        // actual computation
        foo + bar + baz + 42 
      }
    }
  }
}

Мое решение отлично работает здесь, и я могу вернуться раньше с 4 в качестве возвращаемого значения, если захочу. Проблема в том, что мне приходится явно указывать параметр типа [Int], что немного неудобно. Есть ли способ обойти это?


person Jean-Philippe Pellet    schedule 08.06.2011    source источник
comment
Я не думаю, что это всегда плохая практика. Кажется, что его слишком часто используют в языках, которые упрощают его и не предоставляют хороших альтернатив.   -  person Rex Kerr    schedule 08.06.2011
comment
Если вы напишете различие между случаями неглубоко (используя else if), ваши фактические вычисления будут вложены только один раз по сравнению с теми, которые не вложены в ваш хак. В чем проблема с этим? В вашем примере все, что вы сохраняете, — это одно ключевое слово else, но у вас есть все накладные расходы.   -  person Raphael    schedule 08.06.2011
comment
@Raphael Да, в этом примере, но я указал, что рассматриваю случаи, когда, конечно, мне нужно проверить более одного условия — обычно 3 или 4, поэтому мой фактический код быть вложенными 3 или 4 раза.   -  person Jean-Philippe Pellet    schedule 09.06.2011
comment
Вы всегда можете избавиться от этой вложенности (скопировав некоторые условия), но я думаю, вы этого не хотите? В любом случае, ваш вызов EarlyReturn должен быть вложенным, верно? Так что же вы на самом деле хотите сэкономить, кроме вложенности вокруг реальных вычислений?   -  person Raphael    schedule 09.06.2011
comment
@ Рафаэль, я думаю, что мы продолжаем упускать из виду друг друга, поэтому я отредактировал свой пример. Вызов earlyReturn может быть вложенным, но не нуждается в фигурных скобках и может находиться на той же строке. Мой окончательный уровень вложенности вычислений не должен зависеть от количества проверенных ранее условий.   -  person Jean-Philippe Pellet    schedule 09.06.2011


Ответы (2)


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

def earlyReturn[T](ret: T): Any @cpsParam[Any, Any] = shift((k: Any => Any) => ret)
def withEarlyReturn[T](f: => T @cpsParam[T, T]): T = reset(f)
def cpsunit: Unit @cps[Any] = ()

def compute(bool: Boolean) = { 
    val g = withEarlyReturn {
         val a = 1
         if(bool) earlyReturn(4) else cpsunit    
         val b = 1
         earlyReturn2(4, bool)            
         val c = 1
         if(bool) earlyReturn(4) else cpsunit            
         a + b + c + 42
    }
    println(g)  
}

Единственная проблема здесь в том, что вы должны явно использовать cpsunit.

EDIT1: Да, earlyReturn(4, cond = !checkOK) можно реализовать, но он не будет таким общим и элегантным:

def earlyReturn2[T](ret: T, cond: => Boolean): Any @cpsParam[Any, Any] =
                            shift((k: Any => Any) => if(cond) ret else k())

k во фрагменте выше представляет остальную часть вычислений. В зависимости от значения cond мы либо возвращаем значение, либо продолжаем вычисление.

EDIT2: Any chance we might get rid of cpsunit? Проблема здесь в том, что shift внутри инструкции if не допускается без else. Компилятор отказывается преобразовывать Unit в Unit @cps[Unit].

person Vasil Remeniuk    schedule 09.06.2011
comment
Спасибо, очень интересный подход. Не могли бы вы добавить еще несколько пояснений для тех из нас, кто не знаком с продолжениями? - person Jean-Philippe Pellet; 09.06.2011
comment
Есть ли шанс, что мы сможем избавиться от cpsunit, если разрешим вызов типа earlyReturn(4, cond = !checkOK), переместив условие в логический параметр? Или вы имели в виду что-то более элегантное? Наконец: работает ли подход с продолжениями, если несколько из них вложены друг в друга? - person Jean-Philippe Pellet; 09.06.2011
comment
›› работает ли подход с продолжениями, если несколько из них вложены друг в друга? С некоторыми правками да. - person Vasil Remeniuk; 09.06.2011
comment
Поскольку не было предоставлено никакого другого ответа о том, как избавиться от аннотации моего типа, я предполагаю, что мне просто нужно ее написать, и я приму ваш ответ. - person Jean-Philippe Pellet; 02.07.2011

Я думаю, что пользовательское исключение - правильный инстинкт здесь.

person Bill    schedule 09.06.2011