Scala, эквивалентный выражениям where в Haskell?

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

Редактировать:

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

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

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

4 ответа

Решение

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

Местные `вар` и` вал`

В Скала:

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

В Хаскеле:

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

Местный `def`

В Скала

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

В хаскеле

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

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

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

РЕДАКТИРОВАТЬ: Также см. Ответ Daniel C. Sobral для такого примера и некоторые разработки по этому вопросу.

РЕДАКТИРОВАТЬ 2: Добавлено обсуждение lazyvarс и vals.

Ленивый `вар` и` вал`

Edward KMETT справедливо указывалось, что в предложении Хаскелла лень и чистота. Вы можете сделать что-то очень похожее в 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 и достичь аналогичных результатов. Разница в том, что ленивые переменные кэшируются (иначе говоря, только один раз), но def оценивается каждый раз, когда он вызывается.

РЕДАКТИРОВАТЬ 3: Добавлено обсуждение о области видимости, см. Вопрос.

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

Локальные определения имеют область действия блока, в котором они объявлены, как и ожидалось (в большинстве случаев в редких ситуациях они могут выходить из блока, например, при использовании связывания переменных в среднем потоке в циклах for). Поэтому местный 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() метод печати inner scope, Поэтому, да, вы можете поместить определения после того, как они используются внутри методов! Я бы не советовал, так как считаю, что это плохая практика (по крайней мере, на мой взгляд). В файлах классов вы можете расположить определения методов так, как вам нравится, но я бы сохранил все defs внутри функции, прежде чем они будут использованы. А также valс и varс... ну... мне неловко ставить их после того, как их используют.

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

{
// some logic

// some defs

// some other logic, returning the result
}    

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

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

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

Это сложная вещь для реализации на языке, который имеет побочные эффекты. В мире, где нет побочных эффектов, порядок не будет важен (методы не будут зависеть от каких-либо побочных эффектов). Но, поскольку 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's 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 или они defs.

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

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

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

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

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

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

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

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

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

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

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

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
}

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

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

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

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

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

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

Другие вопросы по тегам