Động lực để Scala phân công đánh giá cho Đơn vị hơn là giá trị được giao là gì?


84

Động lực để Scala phân công đánh giá cho Đơn vị hơn là giá trị được giao là gì?

Mô hình phổ biến trong lập trình I / O là làm những việc như sau:

while ((bytesRead = in.read(buffer)) != -1) { ...

Nhưng điều này là không thể trong Scala vì ...

bytesRead = in.read(buffer)

.. trả về Đơn vị, không phải giá trị mới của byteRead.

Có vẻ như một điều thú vị khi bỏ qua một ngôn ngữ chức năng. Tôi đang tự hỏi tại sao nó được thực hiện như vậy?


David Pollack đã đăng một số thông tin đầu tiên, được khá nhiều người tán thành bởi bình luận mà chính Martin Odersky đã để lại về câu trả lời của mình. Tôi nghĩ người ta có thể hiểu câu trả lời của Pollack một cách an toàn.
Daniel C. Sobral

Câu trả lời:


88

Tôi ủng hộ việc có các nhiệm vụ trả lại giá trị được chỉ định thay vì đơn vị. Martin và tôi đã quay đi quay lại về nó, nhưng lập luận của anh ấy là việc đặt một giá trị trên ngăn xếp chỉ để bật ra 95% thời gian là một sự lãng phí mã byte và có tác động tiêu cực đến hiệu suất.


7
Có lý do gì khiến trình biên dịch Scala không thể xem liệu giá trị của phép gán có thực sự được sử dụng hay không và tạo ra mã bytecode hiệu quả cho phù hợp?
Matt R

43
Nó không dễ dàng như vậy khi có sự hiện diện của các bộ định vị: Mỗi bộ cài đặt phải trả về một kết quả, đó là một điều khó khăn khi viết. Sau đó, trình biên dịch phải tối ưu hóa nó, điều này khó có thể thực hiện trên các cuộc gọi.
Martin Odersky

1
Lập luận của bạn có lý, nhưng java & C # lại chống lại điều đó. Tôi đoán bạn đang làm điều gì đó kỳ lạ với mã byte được tạo, sau đó một nhiệm vụ trong Scala sẽ được biên dịch thành tệp lớp và dịch ngược trở lại Java trông như thế nào?
Phương Nguyễn

3
@ PhươngNguyễn Điểm khác biệt là Nguyên tắc truy cập đồng nhất. Trong C # / Java setters (thường) trả về void. Trong Scala foo_=(v: Foo)sẽ trả về Foonếu nhiệm vụ không.
Alexey Romanov

5
@Martin Odersky: làm thế nào về sau: bộ cài đặt còn lại void( Unit), bài tập x = valueđược dịch thành tương đương với x.set(value);x.get(value); trình biên dịch loại bỏ trong việc tối ưu hóa các giai đoạn get-calls nếu giá trị không được sử dụng. Đó có thể là một sự thay đổi đáng hoan nghênh trong bản phát hành Scala chính mới (do không tương thích ngược) và ít gây khó chịu hơn cho người dùng. Bạn nghĩ sao?
Eugen Labun

20

Tôi không rành về thông tin nội bộ về lý do thực tế, nhưng sự nghi ngờ của tôi rất đơn giản. Scala làm cho các vòng lặp có tác dụng phụ trở nên khó sử dụng để các lập trình viên tự nhiên thích hiểu hơn.

Nó thực hiện điều này theo nhiều cách. Ví dụ: bạn không có forvòng lặp nơi bạn khai báo và thay đổi một biến. Bạn không thể (dễ dàng) đột biến trạng thái trên một whilevòng lặp cùng lúc bạn kiểm tra điều kiện, có nghĩa là bạn thường phải lặp lại đột biến ngay trước nó và ở cuối nó. Các biến được khai báo bên trong một whilekhối không thể nhìn thấy được từ whileđiều kiện thử nghiệm, điều này làm cho do { ... } while (...)ít hữu ích hơn nhiều. Và như thế.

Cách giải quyết:

while ({bytesRead = in.read(buffer); bytesRead != -1}) { ... 

Đối với bất cứ điều gì nó là giá trị.

Như một lời giải thích khác, có lẽ Martin Odersky đã phải đối mặt với một vài lỗi rất xấu do việc sử dụng như vậy, và quyết định cấm nó khỏi ngôn ngữ của mình.

BIÊN TẬP

David Pollack đã trả lời bằng một số dữ kiện thực tế, được xác nhận rõ ràng bởi thực tế là chính Martin Odersky đã bình luận câu trả lời của mình, chứng tỏ sự tin cậy đối với lập luận về các vấn đề liên quan đến hiệu suất do Pollack đưa ra.


3
Vì vậy, có lẽ forphiên bản vòng lặp sẽ là: for (bytesRead <- in.read(buffer) if (bytesRead) != -1điều này thật tuyệt vời ngoại trừ việc nó sẽ không hoạt động vì không có foreachwithFiltercó sẵn!
oxbow_lakes

12

Điều này xảy ra như một phần của việc Scala có một hệ thống kiểu "chính thức" hơn. Nói một cách chính thức, nhiệm vụ là một tuyên bố hoàn toàn có tác dụng phụ và do đó nên quay trở lại Unit. Điều này có một số hậu quả tốt đẹp; ví dụ:

class MyBean {
  private var internalState: String = _

  def state = internalState

  def state_=(state: String) = internalState = state
}

Các state_=trở về phương pháp Unit(như sẽ được dự kiến cho một setter) chính vì lợi nhuận chuyển nhượng Unit.

Tôi đồng ý rằng đối với các mẫu kiểu C như sao chép luồng hoặc tương tự, quyết định thiết kế cụ thể này có thể hơi rắc rối. Tuy nhiên, nó thực sự tương đối không có vấn đề nói chung và thực sự đóng góp vào tính nhất quán tổng thể của hệ thống kiểu.


Cảm ơn, Daniel. Tôi nghĩ rằng tôi sẽ thích nó hơn nếu sự nhất quán là cả hai phép gán VÀ bộ cài đặt đều trả về giá trị! (Không có lý do gì mà họ không thể.) Tôi nghi ngờ rằng tôi không tìm hiểu các sắc thái của các khái niệm như một "tuyên bố hoàn toàn có tác dụng phụ".
Graham Lea

2
@Graham: Nhưng sau đó, bạn phải tuân theo sự nhất quán và đảm bảo trong tất cả các bộ thiết lập của mình dù phức tạp đến đâu chúng cũng có thể trả về giá trị mà chúng đã đặt. Điều này sẽ phức tạp trong một số trường hợp và trong các trường hợp khác, tôi nghĩ vậy. (Bạn sẽ trả lại gì trong trường hợp lỗi? Null? - đúng hơn là không. Không có? - thì loại của bạn sẽ là Option [T].) Tôi nghĩ thật khó để nhất quán với điều đó.
Debilski

7

Có lẽ điều này là do nguyên tắc tách lệnh-truy vấn ?

CQS có xu hướng phổ biến ở giao điểm của kiểu lập trình OO và chức năng, vì nó tạo ra sự khác biệt rõ ràng giữa các phương thức đối tượng có hoặc không có tác dụng phụ (tức là làm thay đổi đối tượng). Việc áp dụng CQS cho các nhiệm vụ thay đổi đang đưa nó đi xa hơn bình thường, nhưng ý tưởng tương tự cũng được áp dụng.

Một minh họa ngắn về việc tại sao CQS rất hữu ích: Xem xét một giả thuyết lai ngôn ngữ F / OO với một Listlớp mà có phương pháp Sort, Append, First, và Length. Trong kiểu OO bắt buộc, người ta có thể muốn viết một hàm như sau:

func foo(x):
    var list = new List(4, -2, 3, 1)
    list.Append(x)
    list.Sort()
    # list now holds a sorted, five-element list
    var smallest = list.First()
    return smallest + list.Length()

Trong khi theo phong cách chức năng hơn, nhiều khả năng người ta sẽ viết một cái gì đó như thế này:

func bar(x):
    var list = new List(4, -2, 3, 1)
    var smallest = list.Append(x).Sort().First()
    # list still holds an unsorted, four-element list
    return smallest + list.Length()

Những điều này dường như đang cố gắng làm điều tương tự, nhưng rõ ràng một trong hai phương thức là không chính xác, và nếu không biết thêm về hoạt động của các phương thức, chúng ta không thể biết phương thức nào.

Tuy nhiên, sử dụng CQS, chúng tôi sẽ nhấn mạnh rằng nếu AppendSortthay đổi danh sách, chúng phải trả về loại đơn vị, do đó ngăn chúng tôi tạo lỗi bằng cách sử dụng biểu mẫu thứ hai khi chúng tôi không nên. Do đó, sự hiện diện của các tác dụng phụ cũng trở nên tiềm ẩn trong chữ ký của phương pháp.


4

Tôi đoán điều này là để giữ cho chương trình / ngôn ngữ không có tác dụng phụ.

Những gì bạn mô tả là việc cố ý sử dụng một tác dụng phụ mà trong trường hợp chung được coi là một điều xấu.


Heh. Scala không có tác dụng phụ? :) Ngoài ra, hãy tưởng tượng một trường hợp như thế val a = b = 1(tưởng tượng "ma thuật" valở phía trước b) vs val a = 1; val b = 1;.

Điều này có liên quan gì đến tác dụng phụ, ít nhất là không theo nghĩa được mô tả ở đây: Tác dụng phụ (khoa học máy tính)
Feuermurmel

4

Đây không phải là kiểu tốt nhất để sử dụng một phép gán dưới dạng biểu thức boolean. Bạn thực hiện hai việc cùng một lúc nên thường dẫn đến sai sót. Và việc vô tình sử dụng "=" thay vì "==" được tránh với giới hạn Scalas.


2
Tôi nghĩ đây là một lý do rác rưởi! Như OP đã đăng, mã vẫn biên dịch và chạy: nó không làm những gì bạn có thể mong đợi một cách hợp lý. Đó là một gotcha hơn, không ít hơn một!
oxbow_lakes

1
Nếu bạn viết một cái gì đó như if (a = b) nó sẽ không biên dịch. Vì vậy, ít nhất có thể tránh được lỗi này.
ngừng giảng

1
OP đã không sử dụng '=' thay vì '==', anh ấy đã sử dụng cả hai. Ông hy vọng việc chuyển nhượng để trả về một giá trị mà sau đó có thể được sử dụng, ví dụ, để so sánh với giá trị khác (-1 trong ví dụ)
IttayD

@deamon: nó sẽ biên dịch (ít nhất bằng Java) nếu a và b là boolean. Tôi đã thấy người mới rơi vào bẫy này bằng cách sử dụng if (a = true). Thêm một lý do để thích if (a) đơn giản hơn (và rõ ràng hơn nếu sử dụng tên quan trọng hơn!).
PhiLho 21/10/11

2

Nhân tiện: Tôi thấy thủ thuật while ban đầu thật ngu ngốc, ngay cả trong Java. Tại sao không phải là somethign như thế này?

for(int bytesRead = in.read(buffer); bytesRead != -1; bytesRead = in.read(buffer)) {
   //do something 
}

Đúng là, bài tập xuất hiện hai lần, nhưng ít nhất byteRead nằm trong phạm vi nó thuộc về và tôi không chơi với các thủ thuật chuyển nhượng vui nhộn ...


1
Thủ thuật đó tuy là một mẹo khá phổ biến nhưng nó thường xuất hiện trong mọi ứng dụng đọc qua bộ đệm. Và nó luôn giống như phiên bản của OP.
TWiStErRob

0

Bạn có thể có một giải pháp khác cho điều này, miễn là bạn có một loại tham chiếu cho chuyển hướng. Trong một triển khai ngây thơ, bạn có thể sử dụng những điều sau đây cho các kiểu tùy ý.

case class Ref[T](var value: T) {
  def := (newval: => T)(pred: T => Boolean): Boolean = {
    this.value = newval
    pred(this.value)
  }
}

Sau đó, theo ràng buộc mà bạn sẽ phải sử dụng ref.valueđể truy cập tham chiếu sau đó, bạn có thể viết whilevị ngữ của mình dưới dạng

val bytesRead = Ref(0) // maybe there is a way to get rid of this line

while ((bytesRead := in.read(buffer)) (_ != -1)) { // ...
  println(bytesRead.value)
}

và bạn có thể thực hiện việc kiểm tra chống lại bytesReadmột cách ẩn ý hơn mà không cần phải nhập nó.

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.