Sự khác biệt chính thức trong Scala giữa niềng răng và dấu ngoặc đơn, và khi nào chúng nên được sử dụng?


329

Sự khác biệt chính thức giữa việc truyền đối số cho các hàm trong ngoặc đơn ()và trong dấu ngoặc nhọn là {}gì?

Cảm giác tôi nhận được từ cuốn sách Lập trình trong Scala là Scala khá linh hoạt và tôi nên sử dụng thứ tôi thích nhất, nhưng tôi thấy rằng một số trường hợp biên dịch trong khi những trường hợp khác thì không.

Ví dụ (chỉ có ý nghĩa như một ví dụ; tôi sẽ đánh giá cao bất kỳ phản hồi nào thảo luận về trường hợp chung, không phải ví dụ cụ thể này):

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

=> lỗi: bắt đầu bất hợp pháp biểu thức đơn giản

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

=> tốt.

Câu trả lời:


365

Tôi đã cố gắng một lần để viết về điều này, nhưng cuối cùng tôi đã từ bỏ, vì các quy tắc có phần lan tỏa. Về cơ bản, bạn sẽ phải nắm bắt được nó.

Có lẽ tốt nhất là tập trung vào nơi các dấu ngoặc nhọn và dấu ngoặc đơn có thể được sử dụng thay thế cho nhau: khi truyền tham số cho các lệnh gọi phương thức. Bạn có thể thay thế dấu ngoặc đơn bằng dấu ngoặc nhọn nếu và chỉ khi phương thức mong đợi một tham số duy nhất. Ví dụ:

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

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

Tuy nhiên, có nhiều điều bạn cần biết để nắm bắt tốt hơn các quy tắc này.

Kiểm tra biên dịch tăng với parens

Các tác giả của Spray khuyến nghị parens tròn vì chúng giúp kiểm tra biên dịch tăng lên. Điều này đặc biệt quan trọng đối với DSL như Spray. Bằng cách sử dụng parens, bạn đang nói với trình biên dịch rằng nó chỉ nên được cung cấp một dòng duy nhất; do đó, nếu bạn vô tình cho nó hai hoặc nhiều hơn, nó sẽ phàn nàn. Bây giờ đây không phải là trường hợp với dấu ngoặc nhọn - ví dụ nếu bạn quên một toán tử ở đâu đó, thì mã của bạn sẽ biên dịch và bạn nhận được kết quả không mong muốn và có khả năng là một lỗi rất khó tìm. Dưới đây là kế hoạch (vì các biểu thức là thuần túy và ít nhất sẽ đưa ra cảnh báo), nhưng đưa ra quan điểm:

method {
  1 +
  2
  3
}

method(
  1 +
  2
  3
)

Các biên dịch đầu tiên, thứ hai cho error: ')' expected but integer literal found. Tác giả muốn viết 1 + 2 + 3.

Người ta có thể lập luận rằng nó tương tự đối với các phương thức đa tham số với các đối số mặc định; Không thể vô tình quên dấu phẩy để phân tách các tham số khi sử dụng parens.

Độ dài

Một lưu ý quan trọng thường bị bỏ qua về tính dài dòng. Việc sử dụng dấu ngoặc nhọn chắc chắn sẽ dẫn đến mã dài dòng vì hướng dẫn kiểu Scala nói rõ rằng việc đóng dấu ngoặc nhọn phải nằm trên dòng riêng của chúng:

Niềng răng đóng là trên dòng riêng của nó ngay sau dòng cuối cùng của chức năng.

Nhiều trình định dạng lại tự động, như trong IntelliJ, sẽ tự động thực hiện việc định dạng lại này cho bạn. Vì vậy, hãy cố gắng để sử dụng parens tròn khi bạn có thể.

Ký hiệu thông tin

Khi sử dụng ký hiệu infix, như List(1,2,3) indexOf (2)bạn có thể bỏ dấu ngoặc đơn nếu chỉ có một tham số và viết nó dưới dạng List(1, 2, 3) indexOf 2. Đây không phải là trường hợp ký hiệu chấm.

Cũng lưu ý rằng khi bạn có một tham số duy nhất là biểu thức nhiều mã thông báo, như x + 2hoặc a => a % 2 == 0, bạn phải sử dụng dấu ngoặc đơn để chỉ ra ranh giới của biểu thức.

Bộ dữ liệu

Bởi vì đôi khi bạn có thể bỏ qua dấu ngoặc đơn, đôi khi một tuple cần thêm dấu ngoặc đơn như trong ((1, 2))và đôi khi dấu ngoặc đơn bên ngoài có thể được bỏ qua, như trong (1, 2). Điều này có thể gây nhầm lẫn.

Chức năng / một phần Chức năng chữ với case

Scala có một cú pháp cho chức năng và một phần chức năng chữ. Nó trông như thế này:

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

Những nơi khác mà bạn có thể sử dụng casecâu lệnh là với matchcatchtừ khóa:

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

Bạn không thể sử dụng các casecâu lệnh trong bất kỳ bối cảnh nào khác . Vì vậy, nếu bạn muốn sử dụng case, bạn cần niềng răng xoăn. Trong trường hợp bạn đang tự hỏi điều gì làm nên sự khác biệt giữa một chức năng và một phần chức năng theo nghĩa đen, câu trả lời là: bối cảnh. Nếu Scala mong đợi một chức năng, một chức năng bạn nhận được. Nếu nó mong đợi một chức năng một phần, bạn có được một phần chức năng. Nếu cả hai được mong đợi, nó sẽ đưa ra một lỗi về sự mơ hồ.

Biểu thức và khối

Dấu ngoặc đơn có thể được sử dụng để tạo ra các biểu thức con. Niềng răng xoăn có thể được sử dụng để tạo các khối mã (đây không phải là một hàm theo nghĩa đen, vì vậy hãy cẩn thận khi thử sử dụng nó như một). Một khối mã bao gồm nhiều câu lệnh, mỗi câu lệnh có thể là một câu lệnh nhập, một khai báo hoặc một biểu thức. Nó như thế này:

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

Vì vậy, nếu bạn cần khai báo, nhiều câu lệnh, một importhoặc bất cứ thứ gì tương tự, bạn cần có dấu ngoặc nhọn. Và bởi vì một biểu thức là một câu lệnh, dấu ngoặc đơn có thể xuất hiện bên trong dấu ngoặc nhọn. Nhưng điều thú vị là các khối mã cũng là các biểu thức, vì vậy bạn có thể sử dụng chúng ở bất cứ đâu trong một biểu thức:

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

Vì vậy, vì các biểu thức là các câu lệnh và các khối mã là các biểu thức, mọi thứ bên dưới đều hợp lệ:

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

Nơi họ không thể thay thế cho nhau

Về cơ bản, bạn không thể thay thế {}bằng ()hoặc ngược lại ở bất kỳ nơi nào khác. Ví dụ:

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

Đây không phải là một cuộc gọi phương thức, vì vậy bạn không thể viết nó theo bất kỳ cách nào khác. Chà, bạn có thể đặt dấu ngoặc nhọn bên trong dấu ngoặc đơn cho condition, cũng như sử dụng dấu ngoặc đơn bên trong dấu ngoặc nhọn cho khối mã:

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

Vì vậy, tôi hy vọng điều này sẽ giúp.


53
Đó là lý do tại sao mọi người tranh luận Scala là phức tạp. Tôi muốn gọi mình là một người đam mê Scala.
andyczerwonka

Không phải giới thiệu một phạm vi cho mọi phương pháp tôi nghĩ làm cho mã Scala đơn giản hơn! Lý tưởng nhất là không nên sử dụng phương pháp nào {}- mọi thứ nên là một biểu thức thuần túy duy nhất
samthebest

1
@andyczerwonka Tôi hoàn toàn đồng ý nhưng đó là mức giá tự nhiên và không thể tránh khỏi (?) mà bạn phải trả cho sự linh hoạt và sức mạnh biểu cảm => Scala không quá đắt. Đây là sự lựa chọn đúng đắn cho bất kỳ tình huống cụ thể nào, tất nhiên là một vấn đề khác.
Ashkan Kh. Đức quốc xã

Xin chào, khi bạn nói List{1, 2, 3}.reduceLeft(_ + _)không hợp lệ, bạn có nghĩa là nó có lỗi cú pháp? Nhưng tôi tìm thấy mã có thể biên dịch. Tôi đặt mã của mình ở đây
calvin

Bạn đã sử dụng List(1, 2, 3)trong tất cả các ví dụ, thay vì List{1, 2, 3}. Than ôi, trên phiên bản hiện tại của Scala (2.13), điều này không thành công với một thông báo lỗi khác (dấu phẩy bất ngờ). Bạn có thể phải quay lại 2.7 hoặc 2.8 để nhận lỗi ban đầu.
Daniel C. Sobral

56

Có một vài quy tắc và suy luận khác nhau đang diễn ra ở đây: trước hết, Scala sẽ niềng răng khi tham số là một hàm, ví dụ như trong list.map(_ * 2)niềng răng được suy ra, nó chỉ là một dạng ngắn hơn list.map({_ * 2}). Thứ hai, Scala cho phép bạn bỏ qua dấu ngoặc đơn trong danh sách tham số cuối cùng, nếu danh sách tham số đó có một tham số và đó là một hàm, vì vậy list.foldLeft(0)(_ + _)có thể được viết dưới dạng list.foldLeft(0) { _ + _ }(hoặc list.foldLeft(0)({_ + _})nếu bạn muốn thêm rõ ràng).

Tuy nhiên, nếu bạn thêm casebạn nhận được, như những người khác đã đề cập, một phần chức năng thay vì một chức năng và Scala sẽ không suy ra các dấu ngoặc cho các chức năng một phần, vì vậy list.map(case x => x * 2)sẽ không hoạt động, nhưng cả hai list.map({case x => 2 * 2})list.map { case x => x * 2 }sẽ.


4
Không chỉ của danh sách tham số cuối cùng. Ví dụ, list.foldLeft{0}{_+_}công trình.
Daniel C. Sobral

1
Ah, tôi chắc chắn rằng tôi đã đọc rằng đó chỉ là danh sách tham số cuối cùng, nhưng rõ ràng tôi đã sai! Tốt để biết.
Theo

23

Có một nỗ lực từ cộng đồng để chuẩn hóa việc sử dụng dấu ngoặc và dấu ngoặc đơn, xem Hướng dẫn về Phong cách Scala (trang 21): http://www.codecommit.com/scala-style-guide.pdf

Cú pháp được đề xuất cho các cuộc gọi phương thức bậc cao hơn là luôn luôn sử dụng dấu ngoặc nhọn và bỏ qua dấu chấm:

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

Đối với các cuộc gọi metod "bình thường", bạn nên sử dụng dấu chấm và dấu ngoặc đơn.

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

18
Trên thực tế quy ước là sử dụng niềng răng tròn, liên kết đó là không chính thức. Điều này là do trong lập trình chức năng, tất cả các chức năng chỉ là công dân bậc nhất và do đó KHÔNG nên được đối xử khác nhau. Thứ hai Martin Odersky nói rằng bạn nên cố gắng chỉ sử dụng ghi vào cho nhà điều hành như phương pháp (ví dụ +, --), KHÔNG phương pháp thông thường như takeWhile. Toàn bộ điểm của ký hiệu infix là cho phép DSL và toán tử tùy chỉnh, do đó người ta không nên sử dụng nó trong bối cảnh này.
samthebest

17

Tôi không nghĩ có bất cứ điều gì đặc biệt hoặc phức tạp về niềng răng xoăn trong Scala. Để thành thạo cách sử dụng có vẻ phức tạp của chúng trong Scala, chỉ cần ghi nhớ một vài điều đơn giản:

  1. dấu ngoặc nhọn tạo thành một khối mã, đánh giá đến dòng mã cuối cùng (hầu hết tất cả các ngôn ngữ đều làm điều này)
  2. một hàm nếu muốn có thể được tạo bằng khối mã (theo quy tắc 1)
  3. dấu ngoặc nhọn có thể được bỏ qua cho mã một dòng trừ một mệnh đề trường hợp (lựa chọn Scala)
  4. dấu ngoặc đơn có thể được bỏ qua trong lệnh gọi hàm với khối mã làm tham số (lựa chọn Scala)

Hãy giải thích một vài ví dụ cho ba quy tắc trên:

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. không thực sự đúng trong tất cả các ngôn ngữ. 4. không thực sự đúng trong Scala. Ví dụ: def f (x: Int) = fx
aij

@aij, cảm ơn vì nhận xét. Đối với 1, tôi đã gợi ý sự quen thuộc mà Scala cung cấp cho {}hành vi. Tôi đã cập nhật từ ngữ cho chính xác. Và đối với 4, đó là một chút khó khăn do sự tương tác giữa (){}, như def f(x: Int): Int = f {x}các tác phẩm, và đó là lý do tại sao tôi có lần thứ 5. :)
lcn

1
Tôi có xu hướng nghĩ về () và {} hầu như có thể hoán đổi cho nhau trong Scala, ngoại trừ việc nó phân tích nội dung khác nhau. Tôi thường không viết f ({x}) vì vậy f {x} không cảm thấy muốn bỏ dấu ngoặc đơn nhiều như thay thế chúng bằng curlies. Các ngôn ngữ khác thực sự cho phép bạn bỏ qua các ví dụ, ví dụ, fun f(x) = f xlà hợp lệ trong SML.
aij

@aij, đối xử f {x}như f({x})có vẻ là một lời giải thích tốt hơn cho tôi, vì suy nghĩ (){}thay thế cho nhau là ít trực quan hơn. Nhân tiện, việc f({x})giải thích có phần được hỗ trợ bởi thông số Scala (phần 6.6):ArgumentExprs ::= ‘(’ [Exprs] ‘)’ | ‘(’ [Exprs ‘,’] PostfixExpr ‘:’ ‘_’ ‘*’ ’)’ | [nl] BlockExp
lcn

13

Tôi nghĩ rằng nó là giá trị giải thích việc sử dụng của họ trong các cuộc gọi chức năng và tại sao những điều khác nhau xảy ra. Như ai đó đã nói các dấu ngoặc nhọn xác định một khối mã, đây cũng là một biểu thức có thể được đặt ở nơi biểu thức được mong đợi và nó sẽ được đánh giá. Khi được đánh giá, các câu lệnh của nó được thực thi và giá trị câu lệnh cuối cùng là kết quả của việc đánh giá toàn bộ khối (hơi giống như trong Ruby).

Có được điều đó chúng ta có thể làm những việc như:

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

Ví dụ cuối cùng chỉ là một lời gọi hàm với ba tham số, trong đó mỗi tham số được đánh giá đầu tiên.

Bây giờ để xem cách nó hoạt động với các lệnh gọi hàm, hãy xác định hàm đơn giản lấy hàm khác làm tham số.

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

Để gọi nó, chúng ta cần truyền hàm có một tham số kiểu Int, vì vậy chúng ta có thể sử dụng hàm theo nghĩa đen và truyền nó cho foo:

foo( x => println(x) )

Bây giờ như đã nói trước khi chúng ta có thể sử dụng khối mã thay cho biểu thức, vì vậy hãy sử dụng nó

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

Điều xảy ra ở đây là mã bên trong {} được ước tính và giá trị hàm được trả về dưới dạng giá trị của đánh giá khối, giá trị này sau đó được chuyển cho foo. Đây là về mặt ngữ nghĩa giống như cuộc gọi trước.

Nhưng chúng ta có thể thêm một cái gì đó nữa:

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

Bây giờ khối mã của chúng tôi chứa hai câu lệnh và vì nó được đánh giá trước khi foo được thực thi, điều xảy ra là "Hey" đầu tiên được in, sau đó chức năng của chúng tôi được chuyển đến foo, "Entering foo" được in và cuối cùng là "4" được in .

Điều này có vẻ hơi xấu mặc dù và Scala cho phép chúng ta bỏ qua dấu ngoặc đơn trong trường hợp này, vì vậy chúng ta có thể viết:

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

hoặc là

foo { x => println(x) }

Điều đó trông đẹp hơn nhiều và tương đương với những người trước đây. Ở đây, khối mã vẫn được đánh giá đầu tiên và kết quả đánh giá (là x => println (x)) được truyền dưới dạng đối số cho foo.


1
Có phải chỉ có tôi. nhưng tôi thực sự thích bản chất rõ ràng của foo({ x => println(x) }). Có lẽ tôi quá bế tắc theo cách của mình ...
dade

7

Bởi vì bạn đang sử dụng case, bạn đang xác định một phần chức năng và một phần chức năng yêu cầu dấu ngoặc nhọn.


1
Tôi đã yêu cầu một câu trả lời nói chung, không chỉ là một câu trả lời cho ví dụ này.
Marc-François

5

Kiểm tra biên dịch tăng với parens

Các tác giả của Spray, khuyến nghị rằng parens tròn giúp kiểm tra biên dịch tăng lên. Điều này đặc biệt quan trọng đối với DSL như Spray. Bằng cách sử dụng parens, bạn đang nói với trình biên dịch rằng nó chỉ nên được cung cấp một dòng duy nhất, do đó nếu bạn vô tình đưa ra hai hoặc nhiều hơn, nó sẽ phàn nàn. Bây giờ đây không phải là trường hợp với dấu ngoặc nhọn, ví dụ, nếu bạn quên một toán tử ở đâu đó mã của bạn sẽ biên dịch, bạn sẽ nhận được kết quả không mong muốn và có khả năng là một lỗi rất khó tìm. Dưới đây là ý định (vì các biểu thức là thuần túy và ít nhất sẽ đưa ra cảnh báo), nhưng đưa ra quan điểm

method {
  1 +
  2
  3
}

method(
  1 +
  2
  3
 )

Các biên dịch đầu tiên, thứ hai cho error: ')' expected but integer literal found.tác giả muốn viết 1 + 2 + 3.

Người ta có thể lập luận rằng nó tương tự đối với các phương thức đa tham số với các đối số mặc định; Không thể vô tình quên dấu phẩy để phân tách các tham số khi sử dụng parens.

Độ dài

Một lưu ý quan trọng thường bị bỏ qua về tính dài dòng. Sử dụng dấu ngoặc nhọn chắc chắn sẽ dẫn đến mã dài dòng vì hướng dẫn kiểu scala nói rõ rằng đóng dấu ngoặc nhọn phải nằm trên dòng riêng của họ: http://docs.scala-lang.org/style/declarations.html "... dấu ngoặc nhọn nằm trên dòng riêng ngay sau dòng cuối cùng của hàm. " Nhiều trình định dạng lại tự động, như trong Intellij, sẽ tự động thực hiện việc định dạng lại này cho bạn. Vì vậy, hãy cố gắng để sử dụng parens tròn khi bạn có thể. Ví dụ List(1, 2, 3).reduceLeft{_ + _}:

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

-2

Với niềng răng, bạn có dấu chấm phẩy gây ra cho bạn và dấu ngoặc đơn không. Xem xét takeWhilehàm, vì nó mong đợi hàm một phần, chỉ {case xxx => ??? }là định nghĩa hợp lệ thay vì dấu ngoặc đơn xung quanh biểu thức trường hợp.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.