Call-by-name в Scala против ленивой оценки в Haskell?

Haskell ленивая оценка никогда не будет выполнять больше шагов оценки, чем ожидаемая оценка.

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

Я думал, что вызов по имени примерно эквивалентен ленивой оценке. Почему тогда такая разница во времени гарантируется?

Я предполагаю, что, возможно, язык Haskell указывает, что memoization должна использоваться во время оценки; но в этом случае, почему Scala не делает то же самое?

Ответ 1

В именах, указанных в стратегиях оценки, есть определенная ширина, но они сводятся примерно к этому:

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

    В Scala вы пишете это как:

    scala> def f(x:=> Int): Int = x + x
    scala> f({ println("evaluated"); 1 })
    evaluated
    evaluated
    2
    

    В Haskell у вас нет встроенного способа сделать это, но вы всегда можете отображать значения по названию как функции типа () -> a. Это немного более расплывчато, но из-за ссылочной прозрачности вы не сможете проверить это так, как было бы с помощью Scala (и компилятор мог бы оптимизировать часть "по имени" вашего вызова).

  • вызов по необходимости (ленивый... вид) аргумент не оценивается при вызове функции, но в первый раз необходим. В этот момент он также кэшируется. Впоследствии, всякий раз, когда аргумент нужен снова, кэшированное значение просматривается.

    В Scala вы не объявляете свои аргументы функции ленивыми, вы делаете объявление ленивым:

    scala> lazy x: Int = { println("evaluated"); 1 }
    scala> x + x
    evaluated
    2
    

    В Haskell так работают все функции по умолчанию.

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

    В Scala так работают функции по умолчанию.

    scala> def f(x: Int): Int = x + x
    scala> f({ println("evaluated"); 1 })
    evaluated
    2
    

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

    ghci> :{
    ghci> f :: Int -> Int
    ghci> f !x = x
    ghci> :}
    

Итак, если вызов по требованию (ленивый) делает такую ​​же или меньшую оценку (как любую из других стратегий), зачем использовать что-нибудь еще?

Ленивая оценка трудно объяснить, если у вас нет ссылочной прозрачности, потому что тогда вам нужно точно определить, когда оценивалось ваше ленивое значение. Поскольку Scala построен для взаимодействия с Java, он должен поддерживать обязательное, побочное действие. Как следствие, во многих случаях не рекомендуется использовать lazy в Scala.

Кроме того, lazy имеет служебную нагрузку: вам нужно иметь дополнительное косвенное указание, чтобы проверить, было ли уже оценено значение. В Scala это переводит в кучу больше объектов, что создает еще большую нагрузку на сборщик мусора.

Наконец, бывают случаи, когда ленивая оценка оставляет утечки "пространства". Например, в Haskell сложение большого списка чисел справа путем добавления их вместе является плохой идеей, потому что Haskell будет наращивать эту гигантскую серию ленивых вызовов до (+), прежде чем оценивать их (когда на самом деле вам просто нужно это есть накопитель. Известный пример проблем пространства, которые вы получаете даже в простых контекстах, foldr vs foldl vs foldl'.

Ответ 2

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

Вызов по имени (как вы видели) не эквивалентен ленивой оценке, а замене аргумента типа a аргументом типа () -> a. Такая функция содержит тот же объем информации, что и обычное значение a (типы изоморфны), но для фактического получения этого значения вам всегда нужно применить функцию к аргументу () фиктивный аргумент. Когда вы дважды оцениваете функцию, вы получите дважды тот же результат, но он должен каждый раз вычисляться заново (поскольку автоматическая функция memoising не представляется возможной).

Ленивая оценка эквивалентна замене аргумента типа a аргументом типа, который ведет себя как следующий класс OO:

class Lazy<A> {
  function<A()> computer;
  option<A> containedValue;
 public:
  Lazy(function<A()> computer):
       computer = computer
     , containerValue = Nothing
     {}
  A operator()() {
    if isNothing(containedValue) {
      containedValue = Just(computer());
    }
    return fromJust(containedValue);
  }
}

Это, по сути, всего лишь обертка memoisation вокруг определенного типа по имени по имени. Что не очень приятно, так это то, что эта оболочка фундаментально использует побочные эффекты: когда сначала оценивается ленивое значение, вы должны мутировать containedValue, чтобы представить тот факт, что значение теперь известно. Haskell имеет этот механизм, запеченный в основе его времени исполнения, хорошо протестированный для обеспечения безопасности потоков и т.д. Но на языке, который пытается максимально использовать виртуальную виртуальную машину, это, вероятно, вызовет массивные головные боли, если эти побочные мутации чередуются с явными побочными эффектами. Особенно, потому что действительно интересные приложения ленивости не просто имеют один аргумент функции ленивый (который не будет покупать вас много), но разбросайте ленивые значения по всему позвоночнику глубокой структуры данных. В конце концов, это не только одна функция задержки, которую вы оцениваете позже, чем ввод ленивой функции, это целый поток вложенных вызовов к таким функциям (на самом деле, возможно, бесконечно много!), Поскольку потребляется ленивая структура данных.

Итак, Scala избегает опасностей этого, не делая ничего ленивым по умолчанию, хотя, как говорит Алек, он предлагает ключевое слово lazy, которое в основном добавляет оболочку memoised-function, как указано выше, в значение.

Ответ 3

Это может быть полезно и не подходит для комментариев.

Вы можете написать функцию в Scala, которая ведет себя как Haskell по требованию (для аргументов), создавая аргументы по имени и оценивая их лениво в начале функции:

def foo(x: => Int) = {
  lazy val _x = x
  // make sure you only use _x below, not x
}