15 października 2012

Notacja prefiksowa, infiksowa i static w Scali, Javie i Clojure

Zastanawia mnie, jak programujący w Scali czy Javie zaimplementowaliby metodę max, która zwraca element maksymalny dwóch lub więcej elementów, dla których porównanie zostało zdefiniowane, np. liczb i relacji mniejsze niż.

Mam nieodparte wrażenie, że zaproponowane rozwiązanie, ze względu na klasę tych języków jako obiektowych, w których kładzie się nacisk na definiowanie klas, będzie łamało zasady enkapsulacji i wyniesienia metody na poziom globalny przez zastosowanie słowa kluczowego static. I dlaczego statyczna metoda max w Javie trafiło to do klasy java.lang.Math zamiast java.lang.Number czy podobnie? Interesujący problem.

A wszystko za sprawą zajęć "Functional Programming Principles in Scala". Na wykładzie "Lecture 3.2 - More Fun with Rationals" (około 6:25) pojawiła się implementacja metody max.
class Rational(x: Int, y: Int) {
     ...
     def max(that: Rational) = numer * that.denom < that.numer * denom
}
Ten przykład uzmysłowił mi, że notacja prefiksowa (w Clojure domyślnie lub w Javie i Scali przez metody statyczne) sprawia, że nazwa operacji występuje przed argumentami. Uważam jednak, że użycie static gdziekolwiek w Javie czy Scali, sprowokuje uwagi osób przestrzegających zasad programowania obiektowego, aby nie stosować go, bo niszczy te zasady przez złamanie reguł enkapsulacji. W tym przypadku dobrze byłoby móc zdefiniować statyczną metodę max, która akceptowałaby wiele obiektów Rational i zwracała największy. Sądzę, że ten przykład na wykładzie był niezwykle niefortunny, bo promował zawężenie działania max do wyłącznie dwóch liczb oraz korzystał z notacji infiksowej, która w tym akurat przypadku jest niefortunna.
new Rational(1, 2).max(new Rational(1,3))
zamiast
max(new Rational(1, 2), new Rational(1,3))
Sądzę, że to drugie rozwiązanie odzwierciedla właściwiej fakt porównywania dwóch lub więcej elementów. Możnaby zastanowić się, co miałaby zwrócić ta metoda dla pojedynczej wartości?! Propozycje mile widziane (a dociekliwych zapraszam do przestudiowania rozwiązania w Clojure - clojure.core/max).

I jakby na zamówienie, dzisiaj w skrzynce znalazłem maila od DZone z Refcard dla Scali! Promocja Scali działa pełną parą. Warto rozważyć podobne arkusze dla Clojure, podstaw programowania współbieżnego w Javie i Groovy. Miłej lektury.

11 komentarzy:

  1. Dlaczego "static" łamie reguły enkapsulacji? To czy łamie czy nie jest raczej zdeterminowane przez widoczność metody, niż przez oderwanie od obiektu. Większy problem w tym, że "static" pozwala nam zadeklarować funkcję zamiast metody - coś co istnieje w kontekście obiektu, ale jednak jest od niego niezależne. Jednak czy to rzeczywiście jest taki problem? Co by się stało, gdybyśmy zaczęli pisać w Javie kod bezstanowy i dodawali "static" to każdej metody? Lub chociaż wprowadzili zasadę, że wszystkie pola muszą być "final"? Czy dostalibyśmy paskudny kod łamiący wszystkie reguły obiektowości, czy może kod który jest dużo łatwiejszy w utrzymaniu oraz całkowicie odporny na problemy ze współbieżnością? Tak się czasem zastanawiam...

    OdpowiedzUsuń
  2. Podniosłeś kwestie, które mnie nurtują od dłuższego czasu i nie mam na nie odpowiedzi. Dla mnie "static" zawsze było ucieczką od pełnego OO w Javie (podobnie jak int, long itp.) To w/g mnie taka pozostałość po czasach C.

    Umożliwienie takiej konstrukcji pozwala na wyjście z OO do FP (programowanie funkcyjne). Klasa Math jest dobrym tego przykładem - w zasadzie ma mało wspólnego z klasą, która ma enkapsulować dane i metody, na nich działające, a stanowi po prostu przestrzeń nazewniczą drugiej kategorii (po pakiecie). Tak bardzo przypomina mi namespace w Clojure.

    Wykorzystanie do tego final (jak w String) to jakby potwierdzenie, że w Javie brakuje konstrukcji, które oferuje ówczesna Java, czyli Scala (a może Groovy?), czyli definiowanie funkcji. Przez reguły składniowe w Javie nie jest to możliwe bez ciągania ze sobą całego bagażu klas. W/g mnie to rozwiązuje Scala i pewnie to jest powód, że tak przyciąga programistów javowych.

    OdpowiedzUsuń
  3. Po to się pan Haskell Curry tyle "nakminił" abyśmy nie mieli aż tak wielkich dylematów w takich przypadku :-)

    object math { def max(x: Int, y: Int) = if(x > y) x else y }
    case class Int(a: Int) { def max = math.max(a, _) }

    I tak, to samo (trochę brzydziej), możemy osiągnąć przez powpychanie tu metod statycznych ("funkcjami"), które jednak mają szereg innych wad, np: "nie dziedziczenia się", są "obok", a nie "first class", mogą mieć side effecty etc etc... Także nie chodzi o uniknięcie funkcji, a problemu konkretnej implementacji - statycznych metod w Javie. A tak na codzień, to raczej skłaniam się ku zachowaniu czytelności/utrzymywalności (traits, hurra), niż trzymaniu się zasad "jak w książce". (1,2,3,4).max czy max(1,2,3,4) będzie czytelniejsze niż jakiś łańcuszek z infixowym maxem (1 max 2 max 3 max 4), ale dla innej operacji może być już całkiem odwrotnie :-)

    OdpowiedzUsuń
  4. A ja mam z tą notacją inny problem. "max(x, 10)" jest czytelne do bólu. Ale na pierwszy rzut oka "x max 10" czytam "x, ale nie więcej niż 10" (dosł.: "return x, but at most (max) 10").

    OdpowiedzUsuń
  5. Scala nie ma metod statycznych. Grzegorz

    OdpowiedzUsuń
  6. Jakby nie miała metod statycznych to nie można by utworzyć maina.
    W Scali metody definiowane w obiektach są odpowiednikami metod statycznych z Javy
    object MyClass{
    def myStaticMethod() = {}
    }

    // ... //

    MyClass.myStaticMethod

    OdpowiedzUsuń
    Odpowiedzi
    1. Dzięki Kamil. Grzesiek skutecznie zrujnował moją niewielką wiedzę o Scali swoim komentarzem i przez wiedzę, że on wie nie śmiałem tego podnosić :)

      Usuń
  7. Powtarzam: Scala nie ma metod statycznych, co nie oznacza że nie ma "odpowiedników", ale to nie to samo. W powyższym przykładzie metoda myStaticMethod nie jest metodą statyczną (takich nie ma) tylko zwykłą metodą w obiekcie MyClass. Wywołanie MyClass.myStaticMethod nie jest wywołaniem metody statycznej na klasie MyClass, choćby dlatego, że MyClass nie jest klasą.

    Jeszcze taki przykład, z metodą main:

    class A { def main(args: Array[String]) = println("Hello world") }
    object B extends A

    Jakby metoda main była statyczna to nie byłaby dziedziczona w obiekcie B, bo metody statyczne nie są dziedziczone, i program B by nie zadziałał. A działa:

    $ scala B
    Hello world

    OdpowiedzUsuń
  8. Pozwoliłem sobie napisać artykuł w reakcji na pierwsze zdanie z tego wpisu: http://scala.net.pl/metoda-max/

    OdpowiedzUsuń
  9. Dorzucę fun fact z okazji 2.10.0, dziś publicznie ogłoszonego na scala-annouce (czychała na nas już od 20 grudnia ;-)).
    Dostaliśmy właśnie @static - rzeczywiste pola statyczne w obiektach :]

    OdpowiedzUsuń
    Odpowiedzi
    1. Moim zdaniem zarówno metody zawarte w 'companion objects' jak i @static stosowane nierozważnie mogą powodować wiele zamieszania/szkód.
      Równie dobrze jak na mutowaniu stanu singletona można przepaść zapominając, że statyki w procesie dziedziczenia są raczej przysłaniane, a nie nadpisywane..
      Osobiście trudno będzie mi się przekonać do @static, także ze względu na złe wspomnienia związane z mockowaniem statyków w Javie.

      Usuń