Jaka jest motywacja przypisania Scali do oceny Jednostki, a nie przypisanej wartości?

84

Jaka jest motywacja przypisania Scali do oceny Jednostki, a nie przypisanej wartości?

Typowy wzorzec w programowaniu we / wy polega na wykonywaniu następujących czynności:

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

Ale w Scali nie jest to możliwe, ponieważ ...

bytesRead = in.read(buffer)

.. zwraca Unit, a nie nową wartość bytesRead.

Wydaje się, że interesujące jest pominięcie języka funkcjonalnego. Zastanawiam się, dlaczego tak się stało?

Graham Lea
źródło
David Pollack opublikował kilka informacji z pierwszej ręki, z dużym poparciem w komentarzu samego Martina Oderskiego do swojej odpowiedzi. Myślę, że można spokojnie przyjąć odpowiedź Pollacka.
Daniel C. Sobral,

Odpowiedzi:

88

Opowiadałem się za tym, aby przypisania zwracały przypisaną wartość, a nie jednostkę. Martin i ja omawialiśmy to w kółko, ale jego argument był taki, że umieszczanie wartości na stosie tylko po to, aby usunąć go w 95% przypadków, było marnowaniem kodów bajtowych i miało negatywny wpływ na wydajność.

David Pollak
źródło
7
Czy istnieje powód, dla którego kompilator Scala nie mógł sprawdzić, czy wartość przypisania jest faktycznie używana, i odpowiednio wygenerować efektywny kod bajtowy?
Matt R
43
W obecności seterów nie jest to takie łatwe: każdy seter musi zwrócić wynik, którego pisanie jest trudne. Następnie kompilator musi go zoptymalizować, co jest trudne do wykonania między wywołaniami.
Martin Odersky
1
Twój argument ma sens, ale java i C # są temu przeciwne. Wydaje mi się, że robisz coś dziwnego z wygenerowanym kodem bajtowym, więc jak będzie wyglądać przypisanie w Scali do pliku klasy i dekompilacja z powrotem do Javy?
Phương Nguyễn
3
@ PhươngNguyễn Różnica polega na zasadzie jednolitego dostępu. W C # / Java settery (zwykle) zwracają void. W Scali foo_=(v: Foo)powinien wrócić, Foojeśli przydział tak.
Alexey Romanov
5
@Martin Odersky: co powiesz na następujące: ustawiające pozostają void( Unit), przypisania x = valuesą tłumaczone na odpowiednik x.set(value);x.get(value); kompilator eliminuje podczas optymalizacji faz połączenia getwywołań, jeśli wartość nie była używana. Mogłaby to być mile widziana zmiana w nowej dużej (z powodu wstecznej niekompatybilności) wersji Scala i mniej irytacji dla użytkowników. Co myślisz?
Eugen Labun
20

Nie mam dostępu do informacji wewnętrznych na temat rzeczywistych powodów, ale moje podejrzenie jest bardzo proste. Scala sprawia, że ​​pętle wywołujące efekty uboczne są niewygodne w użyciu, więc programiści naturalnie wolą for-comp R.

Robi to na wiele sposobów. Na przykład nie masz forpętli, w której deklarujesz i modyfikujesz zmienną. Nie możesz (łatwo) zmutować stanu w whilepętli w tym samym czasie, gdy testujesz warunek, co oznacza, że ​​często musisz powtórzyć mutację tuż przed nią i na końcu. Zmienne zadeklarowane wewnątrz whilebloku nie są widoczne z whilewarunku testu, co do { ... } while (...)znacznie zmniejsza ich użyteczność. I tak dalej.

Obejście:

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

Cokolwiek to jest warte.

Jako alternatywne wyjaśnienie, być może Martin Odersky musiał zmierzyć się z kilkoma bardzo brzydkimi błędami wynikającymi z takiego użycia i zdecydował się zakazać tego w swoim języku.

EDYTOWAĆ

David Pollack został odpowiedział z pewnymi faktami, które są wyraźnie zatwierdzone przez fakt, że Martin Odersky sam skomentował swoją odpowiedź, dając wiarę argumentu problemów związanych z wydajnością przedstawianej przez Pollacka.

Daniel C. Sobral
źródło
3
Tak więc przypuszczalnie forwersja pętli wyglądałaby tak: for (bytesRead <- in.read(buffer) if (bytesRead) != -1co jest świetne, z wyjątkiem tego, że nie zadziała, ponieważ nie ma foreachi jest withFilterdostępne!
oxbow_lakes
12

Stało się tak, ponieważ Scala miała bardziej „poprawny formalnie” system typów. Z formalnego punktu widzenia przypisanie jest instrukcją o działaniu ubocznym i dlatego powinno zostać zwrócone Unit. Ma to dobre konsekwencje; na przykład:

class MyBean {
  private var internalState: String = _

  def state = internalState

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

Te state_=powroty Sposób Unit(jak można by oczekiwać na nastawczym) właśnie dlatego powraca przyporządkowania Unit.

Zgadzam się, że w przypadku wzorców w stylu C, takich jak kopiowanie strumienia itp., Ta konkretna decyzja projektowa może być nieco kłopotliwa. Jednak ogólnie rzecz biorąc, jest to stosunkowo bezproblemowe i naprawdę przyczynia się do ogólnej spójności systemu typów.

Daniel Śpiewak
źródło
Dzięki, Daniel. Myślę, że wolałbym, gdyby spójność polegała na tym, że oba przypisania ORAZ ustawiające zwracały wartość! (Nie ma powodu, dla którego nie mogą.) Podejrzewam, że nie narzekam jeszcze na niuanse pojęć, takie jak „stwierdzenie czysto uboczne”.
Graham Lea
2
@Graham: Ale wtedy musiałbyś przestrzegać spójności i upewnić się, że we wszystkich seterach, niezależnie od ich złożoności, zwracają ustawioną wartość. W niektórych przypadkach byłoby to skomplikowane, a w innych, jak sądzę, po prostu błędne. (Co byś zwrócił w przypadku błędu? Null? - raczej nie. Brak? - wtedy twoim typem będzie Option [T].) Myślę, że trudno jest z tym być spójnym.
Dębilski
7

Być może wynika to z zasady separacji poleceń i zapytań ?

CQS wydaje się być popularny na przecięciu OO i funkcjonalnych stylów programowania, ponieważ tworzy oczywiste rozróżnienie między metodami obiektowymi, które mają lub nie mają skutków ubocznych (tj. Zmieniają obiekt). Zastosowanie CQS do przypisań zmiennych prowadzi dalej niż zwykle, ale ta sama idea ma zastosowanie.

Krótki ilustracją dlaczego CQS jest przydatna: Rozważmy hipotetyczny język hybrydowy F / oo z Listklasy, która posiada metody Sort, Append, First, i Length. W imperatywnym stylu OO można by napisać taką funkcję:

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

Podczas gdy w bardziej funkcjonalnym stylu bardziej prawdopodobne jest napisanie czegoś takiego:

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

Wydaje się, że próbują zrobić to samo, ale oczywiście jedna z nich jest niepoprawna i nie wiedząc więcej o zachowaniu metod, nie możemy powiedzieć, która z nich.

Używając CQS, chcielibyśmy jednak nalegać, aby jeśli Appendi zmienili Sortlistę, musieli zwrócić typ jednostki, zapobiegając w ten sposób tworzeniu błędów przez użycie drugiej formy, gdy nie powinniśmy. W związku z tym obecność skutków ubocznych staje się również domniemana w sygnaturze metody.

CA McCann
źródło
4

Sądzę, że dzieje się tak, aby program / język był wolny od skutków ubocznych.

To, co opisujesz, to celowe użycie efektu ubocznego, który w ogólnym przypadku jest uważany za zły.

Jens Schauder
źródło
Heh. Scala bez skutków ubocznych? :) Również wyobrazić sobie sprawę jak val a = b = 1(wyobrazić „magiczny” valz przodu b) vs. val a = 1; val b = 1;.
Nie ma to nic wspólnego z efektami ubocznymi, przynajmniej nie w sensie opisanym tutaj: Efekt uboczny (informatyka)
Feuermurmel.
4

Używanie przypisania jako wyrażenia logicznego nie jest najlepszym stylem. Wykonujesz dwie rzeczy jednocześnie, co często prowadzi do błędów. Przypadkowe użycie znaku „=” zamiast „==” jest unikane dzięki ograniczeniu Scalas.

demon
źródło
2
Myślę, że to bzdura! Jak opublikował OP, kod nadal kompiluje się i działa: po prostu nie robi tego, czego można się spodziewać. To jeszcze jedno słowo więcej, a nie jedno mniej!
oxbow_lakes
1
Jeśli napiszesz coś takiego jak if (a = b), to się nie skompiluje. Więc przynajmniej tego błędu można uniknąć.
demon,
1
OP nie użył znaku „=” zamiast „==”, użył obu. Oczekuje, że przypisanie zwróci wartość, którą można następnie wykorzystać, np. Do porównania z inną wartością (w przykładzie -1)
IttayD
@deamon: skompiluje się (przynajmniej w Javie), jeśli a i b są logiczne. Widziałem początkujących, którzy wpadali w tę pułapkę, używając if (a = true). Jeszcze jeden powód, aby preferować prostsze if (a) (i jaśniejsze, jeśli używasz bardziej znaczącej nazwy!).
PhiLho
2

Przy okazji: uważam, że początkowe sztuczki są głupie, nawet w Javie. Dlaczego nie coś takiego?

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

To prawda, przypisanie pojawia się dwukrotnie, ale przynajmniej bytesRead znajduje się w zakresie, do którego należy, i nie bawię się zabawnymi sztuczkami z przypisaniem ...

Landei
źródło
1
Chociaż sztuczka jest dość powszechna, zwykle pojawia się w każdej aplikacji, która czyta przez bufor. I zawsze wygląda jak wersja OP.
TWiStErRob
0

Możesz obejść ten problem, o ile masz typ referencyjny dla pośredniego. W naiwnej implementacji możesz użyć następujących dla dowolnych typów.

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

Następnie, pod ograniczeniem, którego będziesz musiał użyć, ref.valueaby później uzyskać dostęp do odniesienia, możesz zapisać swój whilepredykat jako

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

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

i możesz przeprowadzić sprawdzanie bytesReadw bardziej niejawny sposób bez konieczności wpisywania go.

Dębilski
źródło