Scala эквивалент Haskell where-clauses?

Можно ли использовать что-то похожее на where-clauses в Scala? Может быть, есть трюк, о котором я не думал?

Edit:

Спасибо за все ваши ответы, они очень ценятся. Подводить итоги: Локальные вары, vals и defs могут использоваться для достижения почти того же. Для ленивой оценки можно использовать ленивый val (с неявным кэшированием) или определения функций. Обеспечение функциональной чистоты предоставляется программисту.

Теперь остается только один вопрос: существует ли способ определения значений или функций после выражений, в которых они используются? Иногда это кажется намного яснее. Это возможно с помощью полей/методов класса или объекта, но он не работает в методах.

Еще одна вещь, о которой до сих пор не упоминалось в ответах. where-clauses также ограничивают объем выражений, определенных в них. Я не нашел способ достичь этого в Scala.

Ответ 1

В Hakell, где предложения содержат локальные определения для функции. Scala не имеет явных предложений where, но такая же функциональность может быть достигнута с помощью локальных var, val и def.

Локальные `var` и` val`

В Scala:

def foo(x: Int, y: Int): Int = {
  val a = x + y 
  var b = x * y
  a - b
}

В Haskell:

foo :: Integer -> Integer -> Integer 
foo x y = a - b
        where 
          a = x + y
          b = x * y

Локальный `def`

В Scala

def foo(x: Int, y: Int): Int = {
  def bar(x: Int) = x * x
  y + bar(x)
}

В Haskell

foo :: Integer -> Integer -> Integer 
foo x y = y + bar x
         where 
           bar x = x * x

Пожалуйста, исправьте меня, если я сделал какие-либо синтаксические ошибки в примере Haskell, так как в настоящее время на этом компьютере не установлен компилятор Haskell:).

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

ИЗМЕНИТЬ. Также см. Daniel ответ для такого примера и некоторую разработку по этому вопросу.

EDIT 2: Добавлено обсуждение lazy var и val s.

Lazy `var` и` val`

Эдвард Кемт правильно ответил, что Хаскелл, где статья имеет лень и чистоту. Вы можете сделать что-то очень похожее в Scala с помощью переменных lazy. Они создаются только при необходимости. Рассмотрим следующий пример:

def foo(x: Int, y: Int) = { 
  print("--- Line 1: ");
  lazy val lazy1: Int = { print("-- lazy1 evaluated "); x^2}
  println();

  print("--- Line 2: ");
  lazy val lazy2: Int = { print("-- lazy2 evaluated "); y^2}
  println();

  print("--- Line 3: ");
  lazy val lazy3: Int = { print("-- lazy3 evaluated ")
    while(true) {} // infinite loop! 
    x^2 + y^2 }
  println();

  print("--- Line 4 (if clause): ");
  if (x < y) lazy1 + lazy2
  else lazy2 + lazy1
}

Здесь lazy1, lazy2 и lazy3 - все ленивые переменные. lazy3 никогда не создается (поэтому этот код никогда не входит в бесконечный цикл), а порядок экземпляра lazy1 и lazy2 зависит от аргументов функции. Например, когда вы вызываете foo(1,2), вы получите lazy1, созданный до lazy2, и когда вы вызовете foo(2,1), вы получите обратное. Попробуйте выполнить код в интерпретаторе Scala и посмотрите распечатку! (Я не стану его здесь, так как этот ответ уже довольно длинный).

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

ИЗМЕНИТЬ 3: Добавлена ​​дискуссия о просмотре, см. вопрос.

Сфера локальных определений

Локальные определения имеют объем блока, в котором они объявлены, как и ожидалось (ну, в большинстве случаев, в редких ситуациях они могут выходить из блока, например, при использовании привязки переменной потока в середине потока для циклов). Поэтому локальные var, val и def могут использоваться для ограничения объема выражения. Возьмем следующий пример:

object Obj {
  def bar = "outer scope"

  def innerFun() {
    def bar = "inner scope"
    println(bar) // prints inner scope
  }

  def outerFun() {
    println(bar) // prints outer scope
  }

  def smthDifferent() {
    println(bar) // prints inner scope ! :)
    def bar = "inner scope"
    println(bar) // prints inner scope
  }

  def doesNotCompile() {
    { 
      def fun = "fun" // local to this block
      42 // blocks must not end with a definition... 
    }
    println(fun)
  }

}

Оба innerFun() и outerFun() ведут себя так, как ожидалось. Определение bar в innerFun() скрывает bar, определенный в охватывающей области. Кроме того, функция fun является локальной для своего закрывающего блока, поэтому ее нельзя использовать иначе. Метод doesNotCompile()... не компилируется. Интересно отметить, что как println() вызывает метод smthDifferent() print inner scope. Поэтому да, вы можете устанавливать определения после того, как они используются внутри методов! Я бы не рекомендовал, хотя, по-моему, это плохая практика (по крайней мере, на мой взгляд). В файлах классов вы можете упорядочить определения методов по своему усмотрению, но я бы сохранил все def внутри функции до их использования. И val и var... ну... Мне неудобно ставить их после того, как они используются.

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

{
// some logic

// some defs

// some other logic, returning the result
}    

Как я уже говорил, вы не можете закончить блок только с помощью // some defs. Здесь Scala немного отличается от Haskell:).

РЕДАКТИРОВАТЬ 4. Разработано для определения материала после их использования, запросив комментарий Kim.

Определение "материала" после использования

Это сложная вещь для реализации на языке, который имеет побочные эффекты. В мире с чисто-без побочных эффектов порядок не будет важен (методы не будут зависеть от каких-либо побочных эффектов). Но, поскольку Scala допускает побочные эффекты, имеет значение то место, где вы определяете функцию. Кроме того, когда вы определяете val или var, правая сторона должна быть оценена на месте, чтобы создать экземпляр val. Рассмотрим следующий пример:

// does not compile :)
def foo(x: Int) = {

  // println *has* to execute now, but
  // cannot call f(10) as the closure 
  // that you call has not been created yet!
  // it similar to calling a variable that is null
  println(f(10))

  var aVar = 1

  // the closure has to be created here, 
  // as it cannot capture aVar otherwise
  def f(i: Int) = i + aVar

  aVar = aVar + 1

  f(10)
}

Пример, который вы даете, работает, но если val lazy или они def s.

def foo(): Int = {
  println(1)
  lazy val a = { println("a"); b }
  println(2)
  lazy val b = { println("b"); 1 }
  println(3)
  a + a
}

В этом примере также хорошо показано кэширование на работе (попробуйте изменить lazy val на def и посмотреть, что произойдет:)

Я все еще в мире, у которого есть побочные эффекты, лучше придерживаться определений, прежде чем использовать их. Легче читать исходный код таким образом.

-- Flaviu Cipcigan

Ответ 2

Похоже, да. Я не буду вдаваться в подробности, поскольку Flaviu уже, но я приведу пример из Википедии.

Haskell:

calc :: String -> [Float]
calc = foldl f [] . words
  where 
    f (x:y:zs) "+" = (y + x):zs
    f (x:y:zs) "-" = (y - x):zs
    f (x:y:zs) "*" = (y * x):zs
    f (x:y:zs) "/" = (y / x):zs
    f xs y = read y : xs

Эти определения - это просто определения, локальные для calc. Итак, в Scala мы сделаем следующее:

def calc(s: String): List[Float] = {
  def f(s: List[Float], op: String) = (s, op) match {
    case (x :: y :: zs, "+") => (y + x) :: zs
    case (x :: y :: zs, "-") => (y - x) :: zs
    case (x :: y :: zs, "*") => (y * x) :: zs
    case (x :: y :: zs, "/") => (y / x) :: zs
    case (xs, y) => read(y) :: xs
  }

  s.words.foldLeft(List[Float]())(f)
}

Так как Scala не имеет эквивалента read, вы можете определить его, как показано ниже, для запуска этого конкретного примера:

def read(s: String) = s.toFloat

Scala тоже не имеет words, к большому огорчению, хотя это легко определить:

implicit toWords(s: String) = new AnyRef { def words = s.split("\\s") }

Теперь определение Haskell более компактно по разным причинам:

  • У него более мощный вывод типа, поэтому ничего, кроме типа calc, не нужно было объявлять. Scala не может этого сделать из-за того, что сознательное дизайнерское решение должно быть объектно-ориентированным с моделью класса.

  • У него есть неявное определение соответствия шаблонов, тогда как в Scala вам нужно объявить функцию, а затем объявить соответствие шаблона.

  • Его обработка каррирования просто превосходит Scala, насколько это касается краткости. Это результат различных решений, о модели класса и операторной нотации, в которой обработка карри считалась не столь важной.

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

Таким образом, существуют различные причины, почему Scala выполняет то, что он делает, хотя мне бы понравилось неявное определение соответствия шаблону.: -)

Ответ 3

Вы можете использовать var и val для предоставления локальных переменных, но это отличается от предложения Haskell where в двух довольно важных аспектах: лень и чистота.

Предложение Haskell where полезно, потому что лень и чистота позволяют компилятору только создавать экземпляры переменных в используемом условии where.

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

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

Вам необходимо вручную указать var и val, которые вы используете, и поместить их перед операторами, которые их используют, подобно операторам ML let.

Ответ 4

Haskell привязывает значения к именам с выражениями let и where. Я уверен, что любые выражения where могут быть стандартизированы в выражения let (независимо от порядка оценки) перед оценкой или генерации кода.

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

В духе стандартизации where -> let одним из способов можно кодировать, где в Scala может быть с макросами (я не пробовал, просто гипотеза) EXPN1 where { EXPN2 }, что EXPN1 является любым допустимым выражением, и EXPN2 может быть любым действительным внутри объявления объекта, расширяющегося до:

object $genObjectname { EXPN2 }
{ import $genObjectName._; EXPN1 }

Пример использования:

sausageStuffer compose meatGrinder where {
  val sausageStuffer = ... // you really don't want to know
  val meatGrinder = ... // not that pretty
}

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