Apa perbedaan formal dalam Scala antara kawat gigi dan tanda kurung, dan kapan harus digunakan?

329

Apa perbedaan formal antara meneruskan argumen ke fungsi dalam tanda kurung ()dan dalam kurung kurawal {}?

Perasaan yang saya dapatkan dari Programming in Scala book adalah Scala cukup fleksibel dan saya harus menggunakan yang paling saya sukai, tetapi saya menemukan bahwa beberapa case dikompilasi sementara yang lain tidak.

Misalnya (hanya dimaksudkan sebagai contoh; Saya akan menghargai setiap tanggapan yang membahas kasus umum, bukan hanya contoh khusus ini):

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

=> error: awal ilegal ekspresi sederhana

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

=> baik.

Jean-Philippe Pellet
sumber

Jawaban:

365

Saya pernah mencoba menulis tentang ini, tetapi pada akhirnya saya menyerah, karena aturannya agak kabur. Pada dasarnya, Anda harus memahami itu.

Mungkin yang terbaik adalah berkonsentrasi di mana kurung kurawal dan kurung dapat digunakan secara bergantian: ketika melewati parameter ke pemanggilan metode. Anda dapat mengganti tanda kurung dengan kurung kurawal jika, dan hanya jika, metode mengharapkan parameter tunggal. Sebagai contoh:

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

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

Namun, masih ada lagi yang perlu Anda ketahui untuk lebih memahami aturan-aturan ini.

Peningkatan pemeriksaan kompilasi dengan parens

Para penulis dari Spray merekomendasikan round parens karena mereka memberikan peningkatan pemeriksaan kompilasi. Ini sangat penting untuk DSL seperti Spray. Dengan menggunakan parens, Anda memberi tahu kompiler bahwa itu hanya boleh diberikan satu baris; karena itu jika Anda secara tidak sengaja memberikannya dua atau lebih, itu akan mengeluh. Sekarang ini bukan masalahnya dengan kurung kurawal - jika misalnya Anda lupa operator di suatu tempat, maka kode Anda akan dikompilasi, dan Anda mendapatkan hasil yang tidak terduga dan berpotensi bug yang sangat sulit ditemukan. Di bawah ini dibuat-buat (karena ekspresinya murni dan setidaknya akan memberi peringatan), tetapi jelaskan:

method {
  1 +
  2
  3
}

method(
  1 +
  2
  3
)

Kompilasi pertama, yang kedua memberi error: ')' expected but integer literal found. Penulis ingin menulis 1 + 2 + 3.

Orang bisa berpendapat bahwa ini serupa untuk metode multi-parameter dengan argumen default; tidak mungkin untuk secara tidak sengaja melupakan koma untuk memisahkan parameter ketika menggunakan parens.

Verbositas

Catatan penting yang sering diabaikan tentang verbositas. Menggunakan kurung kurawal pasti mengarah ke kode verbose karena panduan gaya Scala dengan jelas menyatakan bahwa kurung kurawal harus berada di jalurnya sendiri:

… Kurung penutup berada pada barisnya sendiri segera setelah baris terakhir dari fungsi.

Banyak pemformat ulang otomatis, seperti di IntelliJ, akan secara otomatis melakukan pemformatan ulang ini untuk Anda. Jadi cobalah untuk tetap menggunakan parens bulat ketika Anda bisa.

Notasi Infix

Saat menggunakan notasi infiks, seperti List(1,2,3) indexOf (2)Anda dapat menghilangkan tanda kurung jika hanya ada satu parameter dan tulis sebagai List(1, 2, 3) indexOf 2. Ini bukan kasus notasi titik.

Perhatikan juga bahwa ketika Anda memiliki parameter tunggal yang merupakan ekspresi multi-token, seperti x + 2atau a => a % 2 == 0, Anda harus menggunakan tanda kurung untuk menunjukkan batas-batas ekspresi.

Tuples

Karena terkadang Anda dapat menghilangkan tanda kurung, terkadang tupel membutuhkan tanda kurung tambahan seperti dalam ((1, 2)), dan terkadang tanda kurung luar dapat dihilangkan, seperti di (1, 2). Ini dapat menyebabkan kebingungan.

Function / Partial Function literals dengan case

Scala memiliki sintaks untuk fungsi dan fungsi literal fungsi. Ini terlihat seperti ini:

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

Satu-satunya tempat lain di mana Anda dapat menggunakan casepernyataan adalah dengan matchdan catchkata kunci:

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

Anda tidak dapat menggunakan casepernyataan dalam konteks lain apa pun . Jadi, jika Anda ingin menggunakannya case, Anda perlu kurung kurawal. Jika Anda bertanya-tanya apa yang membuat perbedaan antara fungsi dan fungsi parsial literal, jawabannya adalah: konteks. Jika Scala mengharapkan fungsi, fungsi yang Anda dapatkan. Jika mengharapkan fungsi parsial, Anda mendapatkan fungsi parsial. Jika keduanya diharapkan, ini memberikan kesalahan tentang ambiguitas.

Ekspresi dan Blok

Parenthesis dapat digunakan untuk membuat subekspresi. Kurung kurawal dapat digunakan untuk membuat blok kode (ini bukan fungsi literal, jadi berhati-hatilah untuk mencoba menggunakannya seperti satu). Blok kode terdiri dari beberapa pernyataan, yang masing-masing dapat berupa pernyataan impor, deklarasi atau ekspresi. Bunyinya seperti ini:

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

Jadi, jika Anda membutuhkan deklarasi, banyak pernyataan, importatau semacamnya, Anda perlu kurung kurawal. Dan karena ekspresi adalah pernyataan, tanda kurung dapat muncul di dalam kurung kurawal. Tetapi yang menarik adalah blok kode juga merupakan ekspresi, sehingga Anda dapat menggunakannya di mana saja di dalam ekspresi:

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

Jadi, karena ekspresi adalah pernyataan, dan blok kode adalah ekspresi, semuanya di bawah ini valid:

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

Di mana mereka tidak bisa dipertukarkan

Pada dasarnya, Anda tidak dapat mengganti {}dengan ()atau sebaliknya di tempat lain. Sebagai contoh:

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

Ini bukan panggilan metode, jadi Anda tidak dapat menulisnya dengan cara lain. Nah, Anda bisa meletakkan kurung kurawal di dalam tanda kurung untuk condition, serta menggunakan kurung kurung di dalam kurung kurawal untuk blok kode:

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

Jadi, saya harap ini membantu.

Daniel C. Sobral
sumber
53
Itulah mengapa orang berpendapat bahwa Scala itu kompleks. Dan aku menyebut diriku penggemar Scala.
andyczerwonka
Tidak harus memperkenalkan ruang lingkup untuk setiap metode yang saya pikir membuat kode Scala lebih sederhana! Idealnya tidak ada metode yang harus digunakan {}- semuanya harus menjadi ekspresi murni tunggal
samthebest
1
@andyczerwonka Saya sangat setuju tetapi ini adalah harga alami dan tak terhindarkan (?) Anda membayar untuk fleksibilitas dan kekuatan ekspresif => Scala tidak terlalu mahal. Apakah ini pilihan yang tepat untuk situasi tertentu tentu saja merupakan masalah lain.
Ashkan Kh. Nazary
Halo, ketika Anda mengatakan List{1, 2, 3}.reduceLeft(_ + _)tidak valid, apakah maksud Anda memiliki sintaks yang salah? Tetapi saya menemukan bahwa kode dapat dikompilasi. Saya meletakkan kode saya di sini
calvin
Anda menggunakan List(1, 2, 3)semua contoh, alih-alih List{1, 2, 3}. Sayangnya, pada versi Scala saat ini (2.13), ini gagal dengan pesan kesalahan yang berbeda (koma yang tak terduga). Anda harus kembali ke 2,7 atau 2,8 untuk mendapatkan kesalahan awal, mungkin.
Daniel C. Sobral
56

Ada beberapa aturan dan kesimpulan yang berbeda yang terjadi di sini: pertama-tama, Scala menyimpulkan kawat gigi ketika parameter adalah fungsi, misalnya dalam list.map(_ * 2)kawat gigi disimpulkan, itu hanya bentuk yang lebih pendek list.map({_ * 2}). Kedua, Scala memungkinkan Anda untuk melewati tanda kurung pada daftar parameter terakhir, jika daftar parameter tersebut memiliki satu parameter dan itu adalah fungsi, sehingga list.foldLeft(0)(_ + _)dapat ditulis sebagai list.foldLeft(0) { _ + _ }(atau list.foldLeft(0)({_ + _})jika Anda ingin menjadi lebih eksplisit).

Namun, jika Anda menambahkan caseAnda mendapatkan, seperti yang telah disebutkan orang lain, fungsi parsial bukan fungsi, dan Scala tidak akan menyimpulkan kurung kurawal untuk fungsi parsial, jadi list.map(case x => x * 2)tidak akan berfungsi, tetapi keduanya list.map({case x => 2 * 2})dan list.map { case x => x * 2 }akan.

Theo
sumber
4
Bukan hanya dari daftar parameter terakhir. Misalnya, list.foldLeft{0}{_+_}berfungsi.
Daniel C. Sobral
1
Ah, saya yakin saya sudah membaca bahwa itu hanya daftar parameter terakhir, tetapi jelas saya salah! Senang mendengarnya.
Theo
23

Ada upaya dari komunitas untuk membakukan penggunaan kawat gigi dan tanda kurung, lihat Panduan Gaya Scala (halaman 21): http://www.codecommit.com/scala-style-guide.pdf

Sintaks yang disarankan untuk pemanggilan metode tingkat tinggi adalah selalu menggunakan tanda kurung, dan untuk melewati titik:

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

Untuk panggilan metod "normal", Anda harus menggunakan titik dan tanda kurung.

val result = myInstance.foo(5, "Hello")
olle kullberg
sumber
18
Sebenarnya konvensi adalah menggunakan kawat gigi bundar, tautan itu tidak resmi. Ini karena dalam pemrograman fungsional semua fungsi ADALAH hanya warga negara urutan pertama dan karenanya TIDAK boleh diperlakukan berbeda. Kedua Martin Odersky mengatakan Anda harus mencoba hanya menggunakan infix untuk metode seperti operator (misalnya +, --), BUKAN metode biasa seperti takeWhile. Seluruh titik notasi infiks adalah untuk memungkinkan DSL dan operator kustom, oleh karena itu kita harus menggunakannya dalam konteks ini tidak sepanjang waktu.
samthebest
17

Saya tidak berpikir ada sesuatu yang khusus atau kompleks tentang kurung kurawal di Scala. Untuk menguasai penggunaan yang tampaknya rumit di Scala, ingatlah beberapa hal sederhana:

  1. kurung kurawal membentuk blok kode, yang mengevaluasi ke baris kode terakhir (hampir semua bahasa melakukan ini)
  2. sebuah fungsi jika diinginkan dapat dihasilkan dengan blok kode (mengikuti aturan 1)
  3. kurung kurawal dapat dihilangkan untuk kode satu baris kecuali untuk klausa kasus (pilihan Scala)
  4. tanda kurung dapat dihilangkan dalam panggilan fungsi dengan blok kode sebagai parameter (pilihan Scala)

Mari kita jelaskan beberapa contoh per tiga aturan di atas:

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
sumber
1. sebenarnya tidak benar dalam semua bahasa. 4. sebenarnya tidak benar di Scala. Misalnya: def f (x: Int) = fx
aij
@aij, terima kasih atas komentarnya. Untuk 1, saya menyarankan keakraban yang disediakan Scala untuk {}perilaku tersebut. Saya telah memperbarui kata-kata untuk presisi. Dan untuk 4, ini sedikit rumit karena interaksi antara ()dan {}, sebagai def f(x: Int): Int = f {x}karya, dan itulah sebabnya saya punya yang ke-5. :)
lcn
1
Saya cenderung menganggap () dan {} sebagian besar dapat dipertukarkan di Scala, kecuali bahwa ia mem-parsing isinya secara berbeda. Saya biasanya tidak menulis f ({x}) sehingga f {x} tidak merasa seperti menghilangkan tanda kurung sebanyak menggantinya dengan ikal. Bahasa-bahasa lain benar-benar membiarkan Anda menghilangkan etika, Misalnya, fun f(x) = f xberlaku di SML.
aij
@aij, memperlakukan f {x}sebagai f({x})tampaknya menjadi penjelasan yang lebih baik bagi saya, karena pemikiran ()dan {}dipertukarkan kurang intuitif. Omong-omong, f({x})interpretasinya agak didukung oleh Scala spec (bagian 6.6):ArgumentExprs ::= ‘(’ [Exprs] ‘)’ | ‘(’ [Exprs ‘,’] PostfixExpr ‘:’ ‘_’ ‘*’ ’)’ | [nl] BlockExp
lcn
13

Saya pikir perlu dijelaskan penggunaannya dalam pemanggilan fungsi dan mengapa berbagai hal terjadi. Seperti seseorang sudah mengatakan kurung kurawal mendefinisikan blok kode, yang juga merupakan ekspresi sehingga dapat diletakkan di mana ekspresi diharapkan dan itu akan dievaluasi. Ketika dievaluasi, pernyataannya dieksekusi dan nilai pernyataan terakhir adalah hasil dari evaluasi seluruh blok (agak seperti di Ruby).

Setelah itu kita dapat melakukan hal-hal seperti:

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

Contoh terakhir hanyalah pemanggilan fungsi dengan tiga parameter, yang masing-masing dievaluasi terlebih dahulu.

Sekarang untuk melihat cara kerjanya dengan pemanggilan fungsi mari kita mendefinisikan fungsi sederhana yang mengambil fungsi lain sebagai parameter.

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

Untuk memanggilnya, kita perlu melewati fungsi yang mengambil satu param dari tipe Int, jadi kita bisa menggunakan fungsi literal dan meneruskannya ke foo:

foo( x => println(x) )

Sekarang seperti yang dikatakan sebelumnya kita dapat menggunakan blok kode sebagai ganti ekspresi jadi mari kita gunakan itu

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

Apa yang terjadi di sini adalah bahwa kode di dalam {} dievaluasi, dan nilai fungsi dikembalikan sebagai nilai evaluasi blok, nilai ini kemudian diteruskan ke foo. Secara semantik ini sama dengan panggilan sebelumnya.

Tetapi kita dapat menambahkan sesuatu lagi:

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

Sekarang blok kode kita berisi dua pernyataan, dan karena itu dievaluasi sebelum foo dieksekusi, yang terjadi adalah yang pertama "Hei" dicetak, lalu fungsi kita diteruskan ke foo, "Memasukkan foo" dicetak dan terakhir "4" dicetak .

Ini terlihat agak jelek dan Scala memungkinkan kita untuk melewati tanda kurung dalam kasus ini, sehingga kita dapat menulis:

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

atau

foo { x => println(x) }

Itu terlihat jauh lebih bagus dan setara dengan yang sebelumnya. Di sini masih blok kode dievaluasi terlebih dahulu dan hasil evaluasi (yaitu x => println (x)) diteruskan sebagai argumen ke foo.

Lukasz Korzybski
sumber
1
Apakah hanya aku. tapi saya sebenarnya lebih suka sifat eksplisit dari foo({ x => println(x) }). Mungkin saya terlalu terjebak dalam cara saya ...
dade
7

Karena Anda menggunakan case, Anda mendefinisikan fungsi parsial dan fungsi parsial memerlukan kurung kurawal.

fjdumont
sumber
1
Saya meminta jawaban secara umum, bukan hanya jawaban untuk contoh ini.
Marc-François
5

Peningkatan pemeriksaan kompilasi dengan parens

Para penulis Spray, merekomendasikan bahwa parens bulat memberikan peningkatan pemeriksaan kompilasi. Ini sangat penting untuk DSL seperti Spray. Dengan menggunakan parens, Anda memberi tahu kompiler bahwa itu hanya boleh diberikan satu baris, oleh karena itu jika Anda tidak sengaja memberikannya dua atau lebih, ia akan mengeluh. Sekarang ini bukan masalahnya dengan kurung kurawal, jika misalnya, Anda lupa operator di mana kode Anda akan dikompilasi, Anda mendapatkan hasil yang tidak terduga dan berpotensi menemukan bug yang sangat sulit. Di bawah ini dibikin (karena ekspresinya murni dan setidaknya akan memberi peringatan), tetapi jelaskan

method {
  1 +
  2
  3
}

method(
  1 +
  2
  3
 )

Kompilasi pertama, yang kedua memberi error: ')' expected but integer literal found.penulis ingin menulis 1 + 2 + 3.

Orang bisa berpendapat bahwa ini serupa untuk metode multi-parameter dengan argumen default; tidak mungkin untuk secara tidak sengaja melupakan koma untuk memisahkan parameter ketika menggunakan parens.

Verbositas

Catatan penting yang sering diabaikan tentang verbositas. Menggunakan kurung kurawal pasti mengarah ke kode verbose karena panduan gaya scala jelas menyatakan bahwa kurawal kurung kurawal harus pada baris mereka sendiri: http://docs.scala-lang.org/style/declarations.html "... kurung penutup ada pada barisnya sendiri segera setelah baris terakhir dari fungsi. " Banyak pemformat ulang otomatis, seperti di Intellij, akan secara otomatis melakukan pemformatan ulang ini untuk Anda. Jadi cobalah untuk tetap menggunakan parens bulat ketika Anda bisa. Misalnya List(1, 2, 3).reduceLeft{_ + _}menjadi:

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

Dengan kawat gigi, Anda mendapat tanda koma untuk Anda dan kurung tidak. Pertimbangkan takeWhilefungsi, karena mengharapkan fungsi parsial, hanya {case xxx => ??? }definisi yang valid alih-alih tanda kurung di sekitar ekspresi kasus.

keitine
sumber