Почему "ленивое" ключевое слово, а не тип стандартной библиотеки?

Scala хранит много полезных конструкций, таких как Option и Try в стандартной библиотеке.

Почему ленивый задан особый подход, имея собственное ключевое слово, когда языки, такие как С#, которые не имеют вышеупомянутых типов, предпочитают реализовать его как библиотечную функцию?

Ответ 1

Верно, что вы могли бы определить ленивое значение, например:

object Lazy {  
  def apply[A](init: => A): Lazy[A] = new Lazy[A] {
    private var value = null.asInstanceOf[A]
    @volatile private var initialized = false

    override def toString = 
      if (initialized) value.toString else "<lazy>@" + hashCode.toHexString

    def apply(): A = {
      if (!initialized) this.synchronized {
        if (!initialized) {
          value = init
          initialized = true
        }
      }
      value
    }
  }

  implicit def unwrap[A](l: Lazy[A]): A = l()
}     

trait Lazy[+A] { def apply(): A }

Использование:

val x = Lazy {
  println("aqui")
  42
}

def test(i: Int) = i * i

test(x)

С другой стороны, наличие lazy в качестве модификатора, предоставленного языком, имеет то преимущество, что позволяет ему участвовать в принципе равномерного доступа. Я попытался найти для него запись в блоге, но нет ничего, что выходит за рамки getters и seters. Этот принцип на самом деле более фундаментален. Для значений унифицированы: val, lazy val, def, var, object:

trait Foo[A] {
  def bar: A
}

class FooVal[A](val bar: A) extends Foo[A]

class FooLazyVal[A](init: => A) extends Foo[A] {
  lazy val bar: A = init
}

class FooVar[A](var bar: A) extends Foo[A]

class FooProxy[A](peer: Foo[A]) extends Foo[A] {
  def bar: A = peer.bar
}

trait Bar {
  def baz: Int
}

class FooObject extends Foo[Bar] {
  object bar extends Bar {
    val baz = 42
  }
}

В Scala 2.6 были введены ленивые значения. Существует комментарий Lambda the Ultimate, в котором говорится, что рассуждение может иметь отношение к формализации возможности иметь циклические ссылки:

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

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

Ответ 2

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

Ленивый как библиотека, вероятно, выглядит примерно так:

class LazyVal[T](f: =>T) {
  @volatile private var initialized = false

  /*
   this does not need to be volatile since there will always be an access to the
   volatile field initialized before this is read.
  */
  private var value:T = _ 
  def apply() = {
    if(!initialized) {
      synchronized {
        if(!initialized) {
          value = f
          initialized = true
        }
      }
    }
    value
  }
}

Накладные расходы на это будут объектом закрытия f, который генерирует значение, и другой объект для самого LazyVal. Таким образом, это будет существенным для функции, которая используется так часто, как это.

В CLR у вас есть типы значений, поэтому накладные расходы не так уж плохи, если вы реализуете свой LazyVal как структуру в С#

Однако теперь, когда доступны макросы, может быть хорошей идеей превратить ленивый в библиотечную функцию или, по крайней мере, позволить настроить ленивую инициализацию. Многие случаи использования lazy val не требуют синхронизации потоков, поэтому бесполезно иметь @volatile/synchronized служебные данные при каждом использовании lazy val.