Jaka jest formalna różnica w Scali między nawiasami klamrowymi a nawiasami i kiedy należy ich używać?

329

Jaka jest formalna różnica między przekazywaniem argumentów do funkcji w nawiasach ()i nawiasach klamrowych {}?

Mam wrażenie, że czerpię z książki Programowanie w Scali, że Scala jest dość elastyczna i powinienem użyć tej, którą lubię najbardziej, ale okazuje się, że niektóre przypadki się kompilują, a inne nie.

Na przykład (tylko jako przykład; byłbym wdzięczny za każdą odpowiedź, która omawia ogólny przypadek, a nie tylko ten konkretny przykład):

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

=> błąd: nielegalne rozpoczęcie prostego wyrażenia

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

=> dobrze.

Jean-Philippe Pellet
źródło

Odpowiedzi:

365

Próbowałem kiedyś o tym napisać, ale ostatecznie zrezygnowałem, ponieważ zasady są nieco rozproszone. Zasadniczo musisz się z tym pogodzić.

Być może najlepiej skoncentrować się na tym, gdzie nawiasy klamrowe i nawiasy mogą być używane zamiennie: podczas przekazywania parametrów do wywołań metod. Ty może zastąpić nawias z klamrami, wtedy i tylko wtedy, gdy metoda oczekuje jednego parametru. Na przykład:

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

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

Jednak musisz wiedzieć więcej, aby lepiej zrozumieć te zasady.

Zwiększone sprawdzanie kompilacji za pomocą parens

Autorzy Spray zalecają okrągłe pareny, ponieważ dają zwiększone sprawdzanie kompilacji. Jest to szczególnie ważne w przypadku DSL, takich jak spray. Za pomocą parens informujesz kompilator, że powinien mieć tylko jedną linię; dlatego jeśli przypadkowo podasz dwa lub więcej, będzie narzekać. Teraz nie jest tak w przypadku nawiasów klamrowych - jeśli na przykład zapomnisz gdzieś operatora, kod się skompiluje, a otrzymasz nieoczekiwane wyniki i potencjalnie bardzo trudny do znalezienia błąd. Poniżej wymyślono (ponieważ wyrażenia są czyste i przynajmniej dają ostrzeżenie), ale wskazuje na to:

method {
  1 +
  2
  3
}

method(
  1 +
  2
  3
)

Pierwsza kompiluje, druga daje error: ')' expected but integer literal found. Autor chciał pisać 1 + 2 + 3.

Można argumentować, że podobnie jest w przypadku metod wieloparametrowych z domyślnymi argumentami; nie można przypadkowo zapomnieć przecinka, aby oddzielić parametry podczas korzystania z parens.

Gadatliwość

Ważna często pomijana uwaga na temat gadatliwości. Używanie nawiasów klamrowych nieuchronnie prowadzi do pełnego kodu, ponieważ przewodnik po stylu Scala wyraźnie stwierdza, że ​​zamykanie nawiasów klamrowych musi odbywać się w ich własnej linii:

… Nawias zamykający znajduje się w swoim własnym wierszu bezpośrednio po ostatnim wierszu funkcji.

Wiele auto-reformaterów, takich jak IntelliJ, automatycznie wykona to ponowne formatowanie. Staraj się więc trzymać okrągłe pareny, kiedy możesz.

Notacja Infix

Gdy używasz notacji infix, List(1,2,3) indexOf (2)możesz pominąć nawias, jeśli jest tylko jeden parametr i zapisać go jako List(1, 2, 3) indexOf 2. Nie dotyczy to notacji kropkowej.

Zauważ też, że jeśli masz pojedynczy parametr, który jest wyrażeniem wieloznacznym, takim jak x + 2lub a => a % 2 == 0, musisz użyć nawiasu, aby wskazać granice wyrażenia.

Krotki

Ponieważ czasami można pominąć nawias, czasami krotka wymaga dodatkowego nawiasu, np. W ((1, 2)), a czasami nawias zewnętrzny można pominąć, jak w (1, 2). Może to powodować zamieszanie.

Literały funkcji / funkcji częściowych za pomocą case

Scala ma składnię dla literałów funkcji i częściowych funkcji. To wygląda tak:

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

Jedynymi innymi miejscami, w których można używać caseinstrukcji, są słowa kluczowe matchi catch:

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

Nie można używać caseinstrukcji w żadnym innym kontekście . Tak więc, jeśli chcesz użyć case, potrzebujesz nawiasów klamrowych. Jeśli zastanawiasz się, co sprawia, że ​​różnica między funkcją a funkcją częściową jest dosłowna, odpowiedź brzmi: kontekst. Jeśli Scala spodziewa się funkcji, otrzymasz ją. Jeśli oczekuje funkcji częściowej, otrzymujesz funkcję częściową. Jeśli spodziewane są oba, pojawia się błąd dotyczący niejednoznaczności.

Wyrażenia i bloki

Nawiasów można użyć do wykonania podwyrażeń. Nawiasów klamrowych można używać do tworzenia bloków kodu ( nie jest to dosłowna funkcja, więc wystrzegaj się próbowania używania go jak jednego). Blok kodu składa się z wielu instrukcji, z których każda może być instrukcją importu, deklaracją lub wyrażeniem. To wygląda tak:

{
    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 )

Tak więc, jeśli potrzebujesz deklaracji, wielu instrukcji, importlub czegoś podobnego, potrzebujesz nawiasów klamrowych. Ponieważ wyrażenie jest stwierdzeniem, nawiasy klamrowe mogą pojawiać się w nawiasach. Ale ciekawe jest to, że bloki kodu są również wyrażenia, więc można z nich korzystać w dowolnym miejscu wewnątrz wyrażenia:

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

Ponieważ wyrażenia są wyrażeniami, a bloki kodów są wyrażeniami, wszystko poniżej jest poprawne:

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...

Gdzie nie są wymienne

Zasadniczo, nie można zastąpić {}z ()lub odwrotnie nigdzie indziej. Na przykład:

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

To nie jest wywołanie metody, więc nie możesz napisać go w żaden inny sposób. Cóż, można umieścić nawiasy klamrowe wewnątrz nawiasów dla osób condition, jak również wykorzystania nawiasie wewnątrz nawiasy dla bloku kodu:

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

Mam nadzieję, że to pomoże.

Daniel C. Sobral
źródło
53
Dlatego ludzie twierdzą, że Scala jest złożona. Nazwałbym siebie entuzjastą Scali.
andyczerwonka
Moim zdaniem nie trzeba wprowadzać zakresu dla każdej metody, co upraszcza kod Scala! Najlepiej {}byłoby, gdyby żadna metoda nie była używana - wszystko powinno być pojedynczym czystym wyrażeniem
samthebest
1
@andyczerwonka Całkowicie się zgadzam, ale jest to naturalna i nieunikniona cena (?), którą płacisz za elastyczność, a ekspresyjna moc => Scala nie jest zawyżona. To dobry wybór dla każdej konkretnej sytuacji to oczywiście inna sprawa.
Ashkan Kh. Nazary
Witaj, kiedy mówisz, że List{1, 2, 3}.reduceLeft(_ + _)jest nieprawidłowy, masz na myśli, że ma on błąd składniowy? Ale okazuje się, że ten kod można skompilować. Umieściłem tutaj
calvin
Użyłeś List(1, 2, 3)we wszystkich przykładach zamiast List{1, 2, 3}. Niestety, w bieżącej wersji Scali (2.13) nie powiedzie się to z innym komunikatem o błędzie (nieoczekiwany przecinek). Prawdopodobnie musiałbyś wrócić do wersji 2.7 lub 2.8.
Daniel C. Sobral
56

Dzieje się tu kilka różnych reguł i wniosków: po pierwsze, Scala podaje nawiasy klamrowe, gdy parametr jest funkcją, np. W list.map(_ * 2)nawiasach klamrowych wnioskuje się, że jest to po prostu krótsza forma list.map({_ * 2}). Po drugie, Scala pozwala pominąć nawiasy na liście ostatnich parametrów, jeśli lista parametrów ma jeden parametr i jest funkcją, więc list.foldLeft(0)(_ + _)można go zapisać jako list.foldLeft(0) { _ + _ }(lub list.foldLeft(0)({_ + _})jeśli chcesz być bardziej jawny).

Jeśli jednak dodać casemożna dostać, jak wspominają inni, częściowy funkcja zamiast funkcją i Scala nie będzie wywnioskować szelki dla funkcji częściowych, więc list.map(case x => x * 2)nie będzie działać, ale zarówno list.map({case x => 2 * 2})i list.map { case x => x * 2 }woli.

Theo
źródło
4
Nie tylko ostatnia lista parametrów. Na przykład list.foldLeft{0}{_+_}działa.
Daniel C. Sobral,
1
Ach, byłem pewien, że przeczytałem, że to tylko lista ostatnich parametrów, ale najwyraźniej się myliłem! Dobrze wiedzieć.
Theo,
23

Społeczność stara się ustandaryzować użycie nawiasów klamrowych i nawiasów, patrz Przewodnik po stylu Scala (strona 21): http://www.codecommit.com/scala-style-guide.pdf

Zalecaną składnią dla wywołań metod wyższego rzędu jest zawsze używanie nawiasów klamrowych i pomijanie kropki:

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

Do „normalnych” wywołań metod należy używać kropki i nawiasów.

val result = myInstance.foo(5, "Hello")
olle Kullberg
źródło
18
W rzeczywistości konwencja polega na użyciu okrągłych nawiasów klamrowych, ten link jest nieoficjalny. Wynika to z faktu, że w programowaniu funkcjonalnym wszystkie funkcje SĄ tylko obywatelami pierwszego rzędu i dlatego NIE powinny być traktowane inaczej. Po drugie Martin Odersky mówi powinieneś spróbować użyć tylko infix dla operatora jak metodami (np +, --), a nie zwykłych metod, takich jak takeWhile. Cały punkt notacji infix polega na umożliwieniu DSL i operatorom niestandardowym, dlatego nie należy go używać w tym kontekście przez cały czas.
samthebest,
17

Nie sądzę, żeby w Scali było coś szczególnego lub złożonego w nawiasach klamrowych. Aby opanować pozornie skomplikowane użycie ich w Scali, pamiętaj o kilku prostych rzeczach:

  1. nawiasy klamrowe tworzą blok kodu, którego wynikiem jest ostatni wiersz kodu (robią to prawie wszystkie języki)
  2. w razie potrzeby można wygenerować funkcję z bloku kodu (zgodnie z regułą 1)
  3. nawiasy klamrowe można pominąć dla kodu jednowierszowego, z wyjątkiem klauzuli case (wybór Scala)
  4. nawiasy można pominąć w wywołaniu funkcji z blokiem kodu jako parametrem (wybór Scala)

Wyjaśnijmy kilka przykładów według powyższych trzech zasad:

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
lcn
źródło
1. nie jest prawdą we wszystkich językach. 4. nie jest tak naprawdę w Scali. Np .: def f (x: Int) = fx
aij
@aij, dziękuję za komentarz. Dla 1 sugerowałem znajomość zachowania Scali {}. Zaktualizowałem sformułowanie pod kątem precyzji. A dla 4 jest to trochę trudne ze względu na interakcję między ()i {}, jak def f(x: Int): Int = f {x}działa, i dlatego miałem 5. pozycję. :)
lcn
1
Myślę, że () i {} są w większości wymienne w Scali, z tym wyjątkiem, że analizuje zawartość inaczej. Zwykle nie piszę f ({x}), więc f {x} nie ma ochoty tak bardzo pomijać nawiasy, jak zastępować je nawiasami. Inne języki faktycznie pozwalają pominąć paretheses, na przykład fun f(x) = f xjest ważny w SML.
aij
@aij, traktując f {x}jak f({x})wydaje się być lepszym wytłumaczeniem dla mnie, jak myślenie ()i {}wymienne jest mniej intuicyjne. Nawiasem mówiąc, f({x})interpretacja jest nieco poparta specyfikacją Scali (rozdział 6.6):ArgumentExprs ::= ‘(’ [Exprs] ‘)’ | ‘(’ [Exprs ‘,’] PostfixExpr ‘:’ ‘_’ ‘*’ ’)’ | [nl] BlockExp
lcn
13

Myślę, że warto wyjaśnić ich użycie w wywołaniach funkcji i dlaczego zdarzają się różne rzeczy. Jak ktoś już powiedział, nawiasy klamrowe definiują blok kodu, który jest również wyrażeniem, więc można go umieścić tam, gdzie oczekuje się wyrażenia i zostanie ono ocenione. Podczas oceny wykonywane są jego instrukcje, a wartość instrukcji last jest wynikiem oceny całego bloku (podobnie jak w Ruby).

Dzięki temu możemy robić takie rzeczy jak:

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

Ostatni przykład to tylko wywołanie funkcji z trzema parametrami, z których każdy jest oceniany jako pierwszy.

Teraz, aby zobaczyć, jak to działa z wywołaniami funkcji, zdefiniujmy prostą funkcję, która przyjmuje inną funkcję jako parametr.

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

Aby go wywołać, musimy przekazać funkcję, która przyjmuje jeden parametr typu Int, abyśmy mogli użyć literału funkcji i przekazać go do foo:

foo( x => println(x) )

Teraz, jak powiedziano wcześniej, możemy użyć bloku kodu zamiast wyrażenia, więc użyjmy go

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

To, co się tu dzieje, polega na tym, że kod wewnątrz {} jest analizowany, a wartość funkcji jest zwracana jako wartość oceny bloku, ta wartość jest następnie przekazywana do foo. Jest to semantycznie to samo co poprzednie wywołanie.

Ale możemy dodać coś więcej:

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

Teraz nasz blok kodu zawiera dwie instrukcje, a ponieważ jest on oceniany przed wykonaniem foo, dzieje się tak, że najpierw drukowane jest „Hej”, a następnie nasza funkcja jest przekazywana do foo, drukowane jest „Wprowadzanie foo”, a na końcu drukowane jest „4” .

Wygląda to nieco brzydko, a Scala pozwala nam pominąć nawias w tym przypadku, abyśmy mogli napisać:

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

lub

foo { x => println(x) }

Wygląda to o wiele ładniej i jest odpowiednikiem poprzednich. Tutaj nadal blok kodu jest oceniany jako pierwszy, a wynik oceny (którym jest x => println (x)) jest przekazywany jako argument do foo.

Łukasz Korzybski
źródło
1
Czy to tylko ja. ale tak naprawdę wolę wyraźną naturę foo({ x => println(x) }). Może jestem zbyt
utknięty
7

Ponieważ używasz case, definiujesz funkcję częściową, a funkcje częściowe wymagają nawiasów klamrowych.

fjdumont
źródło
1
Poprosiłem o odpowiedź w ogóle, a nie tylko odpowiedź na ten przykład.
Marc-François,
5

Zwiększone sprawdzanie kompilacji za pomocą parens

Autorzy Spraya zalecają, aby okrągłe pareny zwiększały sprawdzanie kompilacji. Jest to szczególnie ważne w przypadku DSL, takich jak spray. Używając parens, mówisz kompilatorowi, że powinien otrzymać tylko jedną linię, dlatego jeśli przypadkowo dasz mu dwa lub więcej, będzie narzekać. Teraz nie jest tak w przypadku nawiasów klamrowych, jeśli na przykład zapomnisz operatora gdzieś, który skompiluje Twój kod, otrzymasz nieoczekiwane wyniki i potencjalnie bardzo trudny do znalezienia błąd. Poniżej jest wymyślone (ponieważ wyrażenia są czyste i przynajmniej dają ostrzeżenie), ale ma sens

method {
  1 +
  2
  3
}

method(
  1 +
  2
  3
 )

Pierwsza kompilacja, druga daje error: ')' expected but integer literal found.autorowi chęć napisania 1 + 2 + 3.

Można argumentować, że podobnie jest w przypadku metod wieloparametrowych z domyślnymi argumentami; nie można przypadkowo zapomnieć przecinka, aby oddzielić parametry podczas korzystania z parens.

Gadatliwość

Ważna często pomijana uwaga na temat gadatliwości. Używanie nawiasów klamrowych nieuchronnie prowadzi do pełnego kodu, ponieważ przewodnik po stylu Scala wyraźnie stwierdza, że ​​zamykanie nawiasów klamrowych musi odbywać się w ich własnej linii: http://docs.scala-lang.org/style/declarations.html "... nawias klamrowy znajduje się we własnej linii bezpośrednio po ostatnim wierszu funkcji. ” Wiele auto-reformaterów, takich jak Intellij, automatycznie wykona to ponowne formatowanie. Staraj się więc trzymać okrągłe pareny, kiedy możesz. Np. List(1, 2, 3).reduceLeft{_ + _}Staje się:

List(1, 2, 3).reduceLeft {
  _ + _
}
samthebest
źródło
-2

W przypadku nawiasów klamrowych wywołano dla Ciebie średnik, a nawiasów nie. Rozważ takeWhilefunkcję, ponieważ oczekuje ona funkcji częściowej, {case xxx => ??? }jest to tylko poprawna definicja zamiast nawiasów wokół wyrażenia wielkości liter.

keitine
źródło