Какая формальная разница в Scala между фигурными скобками и скобками, и когда они должны использоваться?


329

В чем формальная разница между передачей аргументов функциям в скобках ()и фигурных скобках {}?

Чувство, которое я получил от книги « Программирование в Scala», заключается в том, что Scala довольно гибкая, и я должен использовать ту, которая мне нравится больше всего, но я считаю, что некоторые случаи компилируются, а другие нет.

Например (только в качестве примера; я был бы признателен за любой ответ, который обсуждает общий случай, а не только этот конкретный пример):

val tupleList = List[(String, String)]()
val filtered = tupleList.takeWhile( case (s1, s2) => s1 == s2 )

=> ошибка: недопустимое начало простого выражения

val filtered = tupleList.takeWhile{ case (s1, s2) => s1 == s2 }

=> хорошо.

Ответы:


365

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

Возможно, лучше сконцентрироваться на том, где фигурные скобки и скобки можно использовать взаимозаменяемо: при передаче параметров в вызовы методов. Вы можете заменить круглые скобки круглыми скобками, если и только если метод ожидает один параметр. Например:

List(1, 2, 3).reduceLeft{_ + _} // valid, single Function2[Int,Int] parameter

List{1, 2, 3}.reduceLeft(_ + _) // invalid, A* vararg parameter

Однако вам нужно знать больше, чтобы лучше понять эти правила.

Увеличена проверка компиляции с помощью паренов

Авторы Spray рекомендуют использовать круглые скобки, потому что они повышают проверку компиляции. Это особенно важно для DSL, таких как Spray. Используя parens, вы говорите компилятору, что он должен содержать только одну строку; поэтому, если вы случайно дадите ему два или более, он будет жаловаться. Теперь это не относится к фигурным скобкам - если, например, вы где-то забудете оператор, ваш код скомпилируется, и вы получите неожиданные результаты и, возможно, очень сложную ошибку для поиска. Ниже выдумано (поскольку выражения являются чистыми и, по крайней мере, даст предупреждение), но подчеркивает:

method {
  1 +
  2
  3
}

method(
  1 +
  2
  3
)

Первый компилирует, второй дает error: ')' expected but integer literal found. Автор хотел написать 1 + 2 + 3.

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

многословие

Важное часто пропускаемое примечание о многословии. Использование фигурных скобок неизбежно приводит к многословному коду, поскольку в руководстве по стилю Scala четко указано, что закрывающие фигурные скобки должны быть на отдельной строке:

… Закрывающая скобка находится на отдельной строке сразу после последней строки функции.

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

Инфиксная нотация

При использовании инфиксной нотации, например, List(1,2,3) indexOf (2)вы можете опустить круглые скобки, если есть только один параметр, и записать его как List(1, 2, 3) indexOf 2. Это не случай точечной нотации.

Также обратите внимание, что когда у вас есть один параметр, который является выражением с несколькими токенами, например x + 2или a => a % 2 == 0, вы должны использовать круглые скобки, чтобы указать границы выражения.

Кортеж

Поскольку иногда можно опустить круглые скобки, иногда кортежу требуется дополнительная скобка, например, в ((1, 2)), а иногда внешняя скобка может быть опущена, например, в (1, 2). Это может вызвать путаницу.

Литералы функций / частичных функций с case

Scala имеет синтаксис для литералов функций и частичных функций. Это выглядит так:

{
    case pattern if guard => statements
    case pattern => statements
}

Единственный другие местами , где вы можете использовать caseоператор являются с matchи catchключевыми словами:

object match {
    case pattern if guard => statements
    case pattern => statements
}
try {
    block
} catch {
    case pattern if guard => statements
    case pattern => statements
} finally {
    block
}

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

Выражения и блоки

Скобки могут быть использованы для создания подвыражений. Фигурные скобки могут использоваться для создания блоков кода (это не буквальная функция, поэтому остерегайтесь использовать ее как единое целое). Блок кода состоит из нескольких операторов, каждый из которых может быть оператором импорта, объявлением или выражением. Это выглядит так:

{
    import stuff._
    statement ; // ; optional at the end of the line
    statement ; statement // not optional here
    var x = 0 // declaration
    while (x < 10) { x += 1 } // stuff
    (x % 5) + 1 // expression
}

( expression )

Итак, если вам нужны объявления, несколько операторов importили что-то в этом роде, вам нужны фигурные скобки. А поскольку выражение является утверждением, скобки могут появляться внутри фигурных скобок. Но самое интересное заключается в том , что блоки кода являются также выражения, так что вы можете использовать их в любом месте внутри выражения:

( { var x = 0; while (x < 10) { x += 1}; x } % 5) + 1

Таким образом, поскольку выражения являются операторами, а блоки кодов являются выражениями, все приведенное ниже является действительным:

1       // literal
(1)     // expression
{1}     // block of code
({1})   // expression with a block of code
{(1)}   // block of code with an expression
({(1)}) // you get the drift...

Где они не взаимозаменяемы

В принципе, вы не можете заменить {}с ()или наоборот где - нибудь еще. Например:

while (x < 10) { x += 1 }

Это не вызов метода, поэтому вы не можете написать его другим способом. Ну, вы можете поставить фигурные скобки внутри скобок для condition, а также использовать скобки внутри фигурных скобок для блока кода:

while ({x < 10}) { (x += 1) }

Надеюсь, это поможет.


53
Вот почему люди утверждают, что Скала сложна. И я бы назвал себя энтузиастом Scala.
andyczerwonka

Не нужно вводить область действия для каждого метода, я думаю, что код Scala проще! В идеале ни один метод не должен использоваться {}- все должно быть одним чистым выражением
samthebest

1
@andyczerwonka Я полностью согласен, но это естественная и неизбежная цена (?), которую вы платите за гибкость и выразительную силу => Scala не завышена. Является ли это правильным выбором для любой конкретной ситуации, это, конечно, другое дело.
Ашкан Х. Назарий

Здравствуйте, когда вы говорите List{1, 2, 3}.reduceLeft(_ + _), недействительным, вы имеете в виду, что он имеет синтаксис err? Но я считаю, что код может компилироваться. Я положил свой код здесь
Calvin

Вы использовали List(1, 2, 3)во всех примерах, а не List{1, 2, 3}. Увы, в текущей версии Scala (2.13) это происходит с другим сообщением об ошибке (неожиданная запятая). Возможно, вам придется вернуться к 2.7 или 2.8, чтобы получить исходную ошибку.
Даниэль С. Собрал

56

Здесь происходит несколько различных правил и выводов: во-первых, Scala выводит скобки, когда параметр является функцией, например, в list.map(_ * 2)скобках выводятся, это просто более короткая форма list.map({_ * 2}). Во-вторых, Scala позволяет пропустить скобки в последнем списке параметров, если этот список параметров имеет один параметр и является функцией, поэтому list.foldLeft(0)(_ + _)может быть записан как list.foldLeft(0) { _ + _ }(или list.foldLeft(0)({_ + _})если вы хотите быть более явным).

Однако, если вы добавите case вы получите, как уже упоминалось, частичную функцию вместо функции, и Scala не будет выводить скобки для частичных функций, поэтому list.map(case x => x * 2)не будет работать, но и так list.map({case x => 2 * 2})и list.map { case x => x * 2 }будет.


4
Не только из последнего списка параметров. Например, list.foldLeft{0}{_+_}работает.
Даниэль С. Собрал

1
Ах, я был уверен, что прочитал, что это был только последний список параметров, но явно я ошибся! Хорошо знать.
Тео

23

Сообщество пытается стандартизировать использование скобок и скобок, см. Руководство по стилю Scala (стр. 21): http://www.codecommit.com/scala-style-guide.pdf

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

val filtered = tupleList takeWhile { case (s1, s2) => s1 == s2 }

Для «обычных» вызовов методов вы должны использовать точку и круглые скобки.

val result = myInstance.foo(5, "Hello")

18
На самом деле соглашение заключается в использовании круглых скобок, эта ссылка является неофициальной. Это связано с тем, что в функциональном программировании все функции являются гражданами первого порядка и поэтому НЕ должны рассматриваться по-разному. Во-вторых, Мартин Одерски говорит, что вы должны стараться использовать только инфикс для методов, подобных оператору (например +, --), а НЕ для обычных методов, подобных takeWhile. Весь смысл инфиксной нотации состоит в том, чтобы разрешить DSL и пользовательские операторы, поэтому его следует использовать в этом контексте не всегда.
Самбест

17

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

  1. фигурные скобки образуют блок кода, который вычисляется до последней строки кода (это делают почти все языки)
  2. функция при желании может быть сгенерирована с помощью блока кода (следует правилу 1)
  3. фигурные скобки могут быть опущены для однострочного кода, за исключением предложения case (выбор Scala)
  4. круглые скобки могут быть опущены при вызове функции с блоком кода в качестве параметра (выбор Scala)

Давайте объясним пару примеров согласно вышеупомянутым трем правилам:

val tupleList = List[(String, String)]()
// doesn't compile, violates case clause requirement
val filtered = tupleList.takeWhile( case (s1, s2) => s1 == s2 ) 
// block of code as a partial function and parentheses omission,
// i.e. tupleList.takeWhile({ case (s1, s2) => s1 == s2 })
val filtered = tupleList.takeWhile{ case (s1, s2) => s1 == s2 }

// curly braces omission, i.e. List(1, 2, 3).reduceLeft({_+_})
List(1, 2, 3).reduceLeft(_+_)
// parentheses omission, i.e. List(1, 2, 3).reduceLeft({_+_})
List(1, 2, 3).reduceLeft{_+_}
// not both though it compiles, because meaning totally changes due to precedence
List(1, 2, 3).reduceLeft _+_ // res1: String => String = <function1>

// curly braces omission, i.e. List(1, 2, 3).foldLeft(0)({_ + _})
List(1, 2, 3).foldLeft(0)(_ + _)
// parentheses omission, i.e. List(1, 2, 3).foldLeft(0)({_ + _})
List(1, 2, 3).foldLeft(0){_ + _}
// block of code and parentheses omission
List(1, 2, 3).foldLeft {0} {_ + _}
// not both though it compiles, because meaning totally changes due to precedence
List(1, 2, 3).foldLeft(0) _ + _
// error: ';' expected but integer literal found.
List(1, 2, 3).foldLeft 0 (_ + _)

def foo(f: Int => Unit) = { println("Entering foo"); f(4) }
// block of code that just evaluates to a value of a function, and parentheses omission
// i.e. foo({ println("Hey"); x => println(x) })
foo { println("Hey"); x => println(x) }

// parentheses omission, i.e. f({x})
def f(x: Int): Int = f {x}
// error: missing arguments for method f
def f(x: Int): Int = f x

1. на самом деле не верно для всех языков. 4. На самом деле это не так в Scala. Например: def f (x: Int) = fx
aij

@aij, спасибо за комментарий. Для 1 я предлагал знакомство, которое Scala обеспечивает для {}поведения. Я обновил формулировку для точности. И для 4, это немного сложно из-за взаимодействия между ()и {}, как def f(x: Int): Int = f {x}работает, и именно поэтому у меня был 5-й. :)
августа

1
Я склонен думать о () и {} как о взаимозаменяемых в Scala, за исключением того, что он анализирует содержимое по-разному. Обычно я не пишу f ({x}), поэтому f {x} не хочется прятать круглые скобки, а заменять их фигурными скобками. Другие языки действительно позволяют опускать паретезы, например, fun f(x) = f xдопустимы в SML.
ау

@aij, лечение , f {x}как f({x})это , кажется, лучше объяснение для меня, как мышление ()и {}взаимозаменяемы менее интуитивным. Между прочим, то f({x})интерпретация некоторой степени опирается на Scala спецификации (раздел 6.6):ArgumentExprs ::= ‘(’ [Exprs] ‘)’ | ‘(’ [Exprs ‘,’] PostfixExpr ‘:’ ‘_’ ‘*’ ’)’ | [nl] BlockExp
LCN

13

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

Имея это, мы можем делать такие вещи, как:

2 + { 3 }             // res: Int = 5
val x = { 4 }         // res: x: Int = 4
List({1},{2},{3})     // res: List[Int] = List(1,2,3)

Последний пример - это просто вызов функции с тремя параметрами, каждый из которых оценивается первым.

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

def foo(f: Int => Unit) = { println("Entering foo"); f(4) }

Чтобы вызвать его, нам нужно передать функцию, которая принимает один параметр типа Int, чтобы мы могли использовать литерал функции и передать ее в foo:

foo( x => println(x) )

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

foo({ x => println(x) })

Здесь происходит то, что код внутри {} вычисляется, и значение функции возвращается как значение оценки блока, затем это значение передается в foo. Семантически это то же самое, что и предыдущий вызов.

Но мы можем добавить что-то еще:

foo({ println("Hey"); x => println(x) })

Теперь наш блок кода содержит два оператора, и поскольку он вычисляется перед выполнением foo, происходит то, что сначала печатается «Hey», затем передается наша функция в foo, печатается «Entering foo» и, наконец, «4» ,

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

foo { println("Hey"); x => println(x) }

или

foo { x => println(x) }

Это выглядит намного лучше и эквивалентно первым. Здесь все еще блок кода вычисляется первым, а результат оценки (то есть x => println (x)) передается в качестве аргумента в foo.


1
Это только я. но я на самом деле предпочитаю явную природу foo({ x => println(x) }). Может быть, я слишком застрял в моих путях ...
Дейд

7

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


1
Я попросил ответ вообще, а не просто ответ для этого примера.
Марк-Франсуа

5

Увеличена проверка компиляции с помощью паренов

Авторы Spray рекомендуют, чтобы круглые скобки увеличивали проверку компиляции. Это особенно важно для DSL, таких как Spray. Используя паренсы, вы говорите компилятору, что ему нужно дать только одну строку, поэтому, если вы случайно дали ему две или более, он будет жаловаться. Теперь это не относится к фигурным скобкам, если, например, вы забудете оператор где-то, где ваш код скомпилируется, вы получите неожиданные результаты и потенциально очень трудную ошибку, которую можно найти. Ниже выдумано (так как выражения чистые и, по крайней мере, даст предупреждение), но подчеркивает

method {
  1 +
  2
  3
}

method(
  1 +
  2
  3
 )

Первый компилирует, второй дает error: ')' expected but integer literal found.автору хотел написать 1 + 2 + 3.

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

многословие

Важное часто пропускаемое примечание о многословии. Использование фигурных скобок неизбежно приводит к многословному коду, поскольку в руководстве по стилю scala четко указано, что закрывающие фигурные скобки должны быть на отдельной строке: http://docs.scala-lang.org/style/declarations.html "... закрывающая фигурная скобка находится на отдельной строке сразу после последней строки функции. " Многие автоформаты, как в Intellij, автоматически выполнят это переформатирование для вас. Поэтому старайтесь использовать круглые скобки, когда можете. Например, List(1, 2, 3).reduceLeft{_ + _}становится:

List(1, 2, 3).reduceLeft {
  _ + _
}

-2

С помощью фигурных скобок у вас есть точка с запятой, а скобки - нет. Рассмотрим takeWhileфункцию, так как она ожидает частичную функцию, только {case xxx => ??? }допустимое определение вместо скобок вокруг выражения case.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.