Moje zdjęcie
Software Craftsman's Blog Po Polsku - Marcin Pieciukiewicz
Praktyczne programowanie w Java i Scala

niedziela, 8 września 2013

Przykład użycia AngularJS - prosty kalkulator

Niedawno moją uwagę zwrócił interesujący framework dla JavaScript o nazwie AngularJS. Jest to framework stworzony przez Google w celu ułatwienia tworzenia bogatych aplikacji dla przeglądarek internetowych. W tym artykule chciałbym przedstawić przykład aplikacji stworzonej przy użyciu AngularJS, która będzie wykorzystywała kilka istotnych funkcjonalności dostarczanych przez ten framework. Oczywiście sam dopiero zaczynam poznawać AngularJS, dlatego w tym artykule ograniczę się do najbardziej podstawowych cech AngularJS.

1. Koncepcja aplikacji
Nasza aplikacja będzie bardzo prostym kalkulatorem matematycznym, który umożliwi użytkownikowi wykonywanie czterech podstawowych operacji: dodawania, odejmowania, mnożenia i dzielenia na liczbach całkowitych. Oczekujemy mniej więcej takiego wyglądu tego kalkulatora.



2. Biblioteka AngularJS
AngularJS jest rozprowadzany w postaci pojedyńczego pliku JavaScript, który jest dostępny na stronie angularjs.org. Wystarczy wybrać wersję stabilną (stable) oraz zminimalizowaną (mininified) tego pliku. Ta wersja pliku jest nosi nazwę angular.min.js. W czasie pisania tego artykułu, najnowszą stabilną wersją była 1.0.7.

3. Struktura aplikacji
Nasza aplikacja będzie umieszczona w następującej strukturze katalogów:
Calculator                  Główny katalog projektu
|--lib                      
|  |--js                    Biblioteki JavaScript
|--app
   |--js                    Pliki źródłowe JavaScript
   |--style                 Pliki CSS
   |--view                  Pliki HTML
Po utworzeniu powyższej struktury katalogów kopiujemy pobrany plik angular.min.js do katalogu Calculator/lib/js.
Następnie utwórzmy podstawowe pliki źródłowe w których będziemy definiować naszą aplikację. Będziemy potrzebowali trzech plików:
  • Calculator/index.html - podstawowy plik warstwy widoku
  • Calculator/app/js/app.js - podstawowy plik ze źródłami aplikacji
  • Calculator/app/style/default.css - plik zawierający style kaskadowe

4. Aplikacja AngularJS
W następnym kroku stworzymy szkielet HTML naszej aplikacji, w którym zdefiniujemy podstawowe elementy HTML i dołączymy nasze pliki źródłowe. Wynikowy plik index.html powinien wyglądać następująco:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html ng-app="calculatorApplication">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <script src="lib/js/angular.min.js" type="text/javascript"></script>
    <script src="app/js/app.js" type="text/javascript"></script>
    <link href="app/style/default.css" rel="stylesheet" type="text/css">
</head>
<body>

<div ng-view></div>

</body>
</html>
Poza standardowym nagłówkiem html są tam dwa elementy specyficzne dla aplikacji AngularJS:
  • <html ng-app="calculatorApplication">
  • Parametr ng-app mówi silnikowi AngularJS, że wszystko wewnątrz tagu html jest aplikacją kontrolowaną przez AngularJS. I że jest to moduł o nazwie calculatorApplication.
  • <div ng-view></div>
  • Parametr ng-view wskazuje, że ten element będzie kontenerem w którym AngularJS umieści stworzony widok.
Jak widać framework rozszerza standard HTML o własne atrybuty, które mogą być interpretowane przez silnik AngularJS.

Po utworzeniu struktury HTML aplikacji powinniśmy zdefiniować zachowanie aplikacji. Zrobimy to przez edycję pliku app.js i dodanie następującego kodu:
var app = angular.module("calculatorApplication", []);
Ta konstrukcja stworzy moduł AngularJS nazwany calculatorApplication. Puste nawiasy kwadratowe oznaczają pustą listę zależności wymaganych przez naszą aplikację.
W następnym kroku chcemy zdefiniować podstawowe zachowanie. Chcielibyśmy, aby nasza aplikacja miała stronę startową zawierającą wiadomość powitalną oraz link do kalkulatora.
Ta strona powinna wyglądać następująco (w wolnym tłumaczeniu: To jest strona startowa. Możesz przejść do kalkulatora):

Jak widać adres url strony ma interesującą końcówkę #/home. Jest to po prostu adres podstrony w naszej aplikacji. Jest to standardowy sposób w jaki definiuje się strony w AngularJS.
Następnie chcielibyśmy skojarzyć adres /home z jakąś zawartością. W tym celu dodajmy definicję kontrolera dla tej strony zmieniając plik app.js w następujący sposób:
var app = angular.module("calculatorApplication", []).config(function ($routeProvider) {
    $routeProvider.when("/home", {
        templateUrl: "app/view/home.html",
        controller: "HomeController"
    });

    $routeProvider.otherwise({
        redirectTo: "/home"
    });
});

app.controller("HomeController", function () {
});
W tej części kodu dzieje się kilka interesujących rzeczy. Po pierwsze wywołujemy metodę config i przekazujemy jej jako parametr funkcję, która skonfiguruje naszą aplikację. Ta funkcja przyjmuje parametr $routeProvider, który jest odpowiedzialny za skojarzenie adresów url (m.in. sufiks /home) z odpowiednim zachowaniem. W naszym przypadku chcemy użyć szablonuu home.html jako wzorca widoku i kontrolera HomeController gdy w adresie znajduje się sufiks /home. W każdym innym przypadku chcemy, aby przeglądarka została przekierowana na ten adres.
Dodatkowo zdefiniowaliśmy kontroler o nazwie HomeController, który nie obsługuje żadnej specyficznej funkcjonalności.

Ostatnią rzeczą, którą musimy zrobić jest zdefiniowanie szablonu naszej strony głównej. W tym celu należy stworzyć plik Calculator/app/view/home.html i umieścić w nim następującą zawartość:
<div>
    <div>
        This is home page:
    </div>
    <div>
        You can go to <a href="#/calculator">Calculator</a>.
    </div>
</div>
Jak widać jest to bardzo prosty kod html, który wyświetli wiadomość powitalną i link do strony z kalkulatorem (której jeszcze nie stworzyliśmy).

5. Pierwsza próba działania aplikacji
Spróbujmy teraz sprawdzić, czy nasza aplikacja działa. Można to zrobić na dwa sposoby:
  • Umieścić ją w serwerze http i otworzyć adres pliku index.html w przeglądarce
  • Otworzyć plik index.html prosto z systemu plików
Na razie wykorzystajmy tę drugą możliwość ;)

Uwaga!
Z powodu zasad bezpieczeństwa niektóre przeglądarki internetowe mogą blokować dostęp do plików lokalnych dla wywołań AJAX z poziomu javascript'u. Ten przypadek występuje przynajmniej w przeglądarkach Google Chrome oraz Internet Explorer. Nie wiem jak to rozwiązać w przypadku IE, ale jeżeli chcesz testować aplikację z Chroma, musisz uruchomić przeglądarkę z parametrem --allow-file-access-from-files jak zostało to opisane w pytaniu na stackoverflow. To rozwiązanie nie jest konieczne jeżeli dostajesz się do aplikacji przez serwer http.
W przypadku Mozilla Firefox nasza aplikacja powinna działać poprawnie bez dodatkowych zmian.

6. Strona kalkulatora
Żeby dodać kalkulator do naszej aplikacji musimy stworzyć szablon html, który będzie definiował wygląd naszego kalkulatora. Umieścimy go w pliku Calculator/app/view/calculator.html:
<div class="calculator">
    <div class="display">0</div>
    <div class="digitsKeyboard">
        <div class="digitKey">1</div>
        <div class="digitKey">2</div>
        <div class="digitKey">3</div>
        <div class="digitKey">4</div>
        <div class="digitKey">5</div>
        <div class="digitKey">6</div>
        <div class="digitKey">7</div>
        <div class="digitKey">8</div>
        <div class="digitKey">9</div>
        <div class="digitKey">0</div>
        <div class="equalSignKey">=</div>
    </div>
    <div class="operationsKeyboard">
        <div class="operationKey">/</div>
        <div class="operationKey">*</div>
        <div class="operationKey">+</div>
        <div class="operationKey">-</div>
    </div>
</div>
Kalkulator składa się z wyświetlacza display, klawiatury z cyframi i znakiem równości digitsKeyboard oraz klawiatury z klawiszami operacji matematycznych operationsKeyboard. Dodatkowo musimy zdefinować styl CSS, który będzie opisywał szczegółowy wygląd naszego kalkulatora. W tym celu należy umieścić następujący kod CSS w pliku Calculator/app/style/default.css:
.calculator {
    display: inline-block;
    background-color: black;
    height: 240px;
    width: 208px;
    overflow: hidden;
}

.display {
    background-color: #DDDDDD;
    border: 1px solid black;
    font-family: monospace;
    font-size: 27px;
    font-weight: bold;
    height: 30px;
    padding: 0 4px;
    text-align: right;
    width: 198px;
    overflow: hidden;
}

.digitsKeyboard {
    overflow: hidden;
    width: 156px;
    float: left;
}

.digitKey {
    width: 50px;
    height:50px;
    background-color: #AABBCC;
}

.equalSignKey {
    width: 102px;
    height: 50px;
    background-color: #AACCBB;
}

.operationsKeyboard {
    overflow: hidden;
    width: 52px;
    height:208px;
    float: left;
}

.operationKey {
    width: 50px;
    height:50px;
    background-color: #CCAABB;
}

.digitKey, .equalSignKey, .operationKey {
    cursor: pointer;
    font-family: monospace;
    font-size: 33px;
    border: 1px solid black;
    line-height: 50px;
    text-align: center;
    float: left;
}

.digitKey:hover, .equalSignKey:hover, .operationKey:hover {
    opacity: 0.8;
}
.digitKey:active, .equalSignKey:active, .operationKey:active {
    opacity: 0.7;
}

Ostatnią rzeczą do wykonania jest zmapowanie adresu /calculator w aplikacji. W tym celu musimy dodać stronę kalkulatora do $routeProvider w ten sam sposób w jaki zdefiniowaliśmy stronę główną. Należy więc dodać poniższy kod do pliku Calculator/app/js/app.js:
$routeProvider.when("/calculator", {
    templateUrl: "app/view/calculator.html",
    controller: "CalculatorController"
}); 
Oraz zdefiniować nowy, pusty (na razie) kontroler kalkulatora:
app.controller("CalculatorController", function () {
});
Możesz teraz spróbować otworzyć kalkulator w przeglądarce. Wystarczy otworzyć plik index.html i kliknąć w link kalkulatora. Wtedy powinieneś zobaczyć kalkulator na ekranie.

7. Definicja stałych wartości
Dodamy teraz trochę życia do kontrolera CalculatorController. Żeby to zrobić zacznijmy od zmiany funkcji przekazanej w deklaracji kontrolera:
app.controller("CalculatorController", function ($scope) {
});
Jak widać dodaliśmy parametr $scope do definicji funkcji. Jest to obiekt w którym przechowywany jest stan naszej aplikacji. Dla naszych potrzeb wystarczy wiedzieć, że możemy zdefiniować w nim dowolne pole, które będzie dostępne w innych miejscach aplikacji.

Zacznijmy od zdefiniowania listy klawiszy kalkulatora, wraz z parametrami przez umieszczenie tego kodu w funkcji kontrolera:
Klawisze cyfr
$scope.digitKeys = [
    {label: "1", value: 1}, {label: "2", value: 2}, {label: "3", value: 3},
    {label: "4", value: 4}, {label: "5", value: 5}, {label: "6", value: 6},
    {label: "7", value: 7}, {label: "8", value: 8}, {label: "9", value: 9},
    {label: "0", value: 0}
];
Wykorzystaliśmy pole digitKeys do zdefiniowania tablicy obiektów reprezentujących każdą cyfrę na klawiaturze kalkulatora. Każdy klawisz ma przypisaną etykietę label, która będzie wyświetlana na danym klawiszu oraz wartość value, która będzie wykorzystana do obliczeń.
Klawisz równości
$scope.equalSignKey = {label: "="};
Tutaj zdefiniowaliśmy klawisz równości, który wymaga jedynie podania etykiety. Ta definicja została stworzona jedynie po to, aby zachować spójność w sposobie definiowana klawiszy. Innym rozwiązaniem byłoby umieszczenie znaku = bezpośrednio w pliku html.
Klawisze operacji
$scope.operationKeys = [
    {label: "/", operation: function (a, b) {return a / b}},
    {label: "*", operation: function (a, b) {return a * b}},
    {label: "+", operation: function (a, b) {return a + b}},
    {label: "-", operation: function (a, b) {return a - b}}
];
Ostatni zestaw klawiszy reprezentuje operacje matematyczne. Jak widać, w każdym obiekcie umieściliśmy funkcję, dzięki czemu będziemy mogli w łatwy sposób wykonać żądaną operację.

8. Dynamiczne generowanie klawiszy kalkulatora
Po zdefiniowaniu klawiszy w kontrolerze CalculatorController możemy je użyć do uproszczenia naszego szablonu html przez dynamiczne generowanie klawiszy. Zacznijmy od najprostrzego przypadku, czyli przycisku równości:
<div class="equalSignKey">
    {{ equalSignKey.label }}
</div>
W tym przypadku wykorzystaliśmy konstrukcję {{ }}, żeby przekazać AngularJS że w tym miejscu chcemy umieścić wartość equalSignKey.label. Angular wyszuka tę wartość w obiekcie $scope i powiąże ją z odpowiednią częścią pliku html.

Podobnie postąpimy w przypadku klawiszy cyfr, ale w tym celu wykorzystamy dyrektywę o nazwie ng-repeat, która umożliwia iterowanie po tablicy elementów i stworzenie tagu html dla każdego z nich:
<div class="digitKey" ng-repeat="key in digitKeys">
    {{ key.label }}
</div>
W tym przypadku AngularJS iteruje po elementach z tablicy $scope.digitKeys i przypisuje kolejne elementy do zmiennej key. Dzięki temu nie musimy powtarzać kodu html dla każdego klawisza.

Postąpimy tak samo dla klawiszy operacji:
<div class="operationKey" ng-repeat="key in operationKeys">
     {{ key.label }}
</div>
A więc wynikowy plik calculator.html powinien wyglądać następująco:
<div class="calculator">
    <div class="display">0</div>
    <div class="keyboard">
        <div class="digitKey" ng-repeat="key in digitKeys">
            {{ key.label }}
        </div>
        <div class="equalSignKey" ng-click="compute()">
            {{ equalSignKey.label }}
        </div>
    </div>
    <div class="operations">
        <div class="operationKey" ng-repeat="key in operationKeys">
            {{ key.label }}
        </div>
    </div>
</div>

W tym momencie można ponownie przetestować czy nasz kalkulator jest wyświetlany poprawnie.

9. Działanie kalkulatora
Teraz chcemy, aby nasz kalkulator zaczął działać jak prawdziwy, dlatego musimy zdefiniować zmienne, które będą przechowywały aktualny stan kalkulatora. Potrzebujemy pięć takich zmiennych:
  • displayValue - aktualna wartość wyświetlana na ekranie kalkulatora
  • valueA - pierwsza (lewa) wartość wykorzystywana do obliczeń
  • valueB - druga(prawa) wartość wykorzystywana do obliczeń
  • selectedOperation - która operacja matematyczna została wybrana przez użytkownika
  • clearValue - czy naciśnięcie następnej cyfry powinno wyczyścić ekran?
Zdefiniujmy je w obiekcie $scope i zainicjujmy je domyślnymi wartościami. Dodajmy ten kod wewnątrz kontrolera CalculatorController:
$scope.displayValue = 0;
$scope.valueA = 0;
$scope.valueB = 0;
$scope.selectedOperation = null;
$scope.clearValue = true;

10. Powiązanie zmiennej displayValue z wyświetlaną wartością
Gdy mamy już zmienną displayValue, chcielibyśmy, aby została wyświetlona na ekranie naszego kalkulatora. I to jest właśnie miejsce gdzie najlepsza cecha AngularJS wchodzi do gry. Wystarczy umieścić znacznik {{ displayValue }} wewnątrz calculator.html zmieniając <div class="display">0</div> tak, aby wyglądał tak:
<div class="display">{{ displayValue }}</div>
To wszystko!
Od tego momentu AngularJS będzie robił swoje sztuczki, zapewniając, że wyświetlana wartość będzie równa wartości zmiennej. Jeżeli zmienisz wartość przypisaną do zmiennej, automatyczne zmieni się wartość wyświetlana na ekranie kalkulatora.
To nie wszystko, gdybyśmy umożliwili użytkownikowi bezpośrednią edycję ekranu, to AngularJS automatycznie zmieniłby wartość zmiennej. Czyli takie powiązanie działa w obie strony, co znacznie upraszcza pisanie naszej aplikacji.

11. Zachowanie przycisków kalkulatora
Mamy już wszystkie elementy kalkulatora na miejscu, czas więc tknąć w niego trochę życia. Głównym pomysłem jest skojarzenie przycisków z odpowiednim zachowaniem. Aby to zrobić użyjemy dyrektywy ng-click. Zmieńmy szablon calculator.html:

<div class="calculator">
    <div class="display">{{ displayValue }}</div>
    <div class="digitsKeyboard">
        <div class="digitKey" ng-repeat="key in digitKeys" 
                ng-click="digitClicked(key.value)">
            {{ key.label }}
        </div>
        <div class="equalSignKey" ng-click="compute()">
            {{ equalSignKey.label }}
        </div>
    </div>
    <div class="operationsKeyboard">
        <div class="operationKey" ng-repeat="key in operationKeys" 
                ng-click="operationClicked(key.operation)">
            {{ key.label }}
        </div>
    </div>
</div>

W tej części kodu dodaliśmy trzy dyrektywy three ng-click, które zawierają proste wywołania funkcji JavaScript:
  • Dla klawiszy z cyframi: ng-click="digitClicked(key.value)"
  • Dla klawiszy operacji: ng-click="operationClicked(key.operation)"
  • Dla znaku równości: ng-click="compute()"
Jak widać mogliśmy przekazać dane zdefiniowane wcześniej w zmiennych AngularJS w ten sam sposób w jaki przekazywaliśmy je wcześniej w blokach {{ }}. Chcemy, aby naciśnięcie konkretnego przycisku wywołało odpowiednią, przypisaną do niego funkcję.

Zdefiniujmy teraz te trzy funkcje wewnątrz kontrolera CalculatorController. Co ważne, te funkcje zostaną zdefiniowane jako część obiektu $scope:
  • Funkcja do obsługi przycisków z cyframi:
$scope.digitClicked = function (digit) {
    if ($scope.clearValue) {
        $scope.displayValue = digit;
        $scope.clearValue = false;
    } else {
        $scope.displayValue = $scope.displayValue * 10 + digit;
    }
    $scope.valueB = $scope.displayValue
};
Po kliknięciu w przycisk cyfry chcemy zaktualizować wartość wyświetlaną na ekranie, przez zamianę aktualnie wyświetlanej liczby, lub przez zwiększenie wyświetlanej liczby o kolejną cyfrę (wystarczy pomnożyć wyświetlaną liczbę przez 10 i do wyniku dodać wybraną cyfrę). Dodatkowo musimy zapamiętać wyświetlaną liczbę jako drugą wartość, którą wkyorzystamy do wykonania operacji matematycznej.
  • Funkcja do obsługi przycisków operacji:
$scope.operationClicked = function (operation) {
    $scope.selectedOperation = operation;
    $scope.valueA = $scope.displayValue;
    $scope.valueB = $scope.displayValue;
    $scope.clearValue = true;
};
W przypadku, gdy użytkownik kliknie w przycisk operacji musimy po prostu zapamiętać, którą operację wybrał i przypisać wyświetlaną wartość do obydwu zmiennych, które będziemy mogli wykorzystać do wykonania tej operacji. Dodatkowo musimy ustawić flagę clearValue, tak że przy następnym kliknięciu w cyfrę zastąpi ona wyświetlaną na ekranie liczbę.
  • Funkcja do obsługi przycisku równości:
$scope.compute = function () {
    if($scope.selectedOperation != null) {
        $scope.displayValue = Math.floor(
                       $scope.selectedOperation($scope.valueA, $scope.valueB));
        $scope.clearValue = true;
        $scope.valueA = $scope.displayValue;
    }
}
Ostatnia funkcja jest odpowiedzialna za wyliczenie wyniku, jeżeli wcześniej wybrano operację (przypisaną do pola $scope.selectedOperation). Jest to wykonywane przez proste wywołanie zapamiętanej funkcji. Dodatkowo ustawiana jest flaga clearValue, a wynik obliczenia jest zapamiętywany jako pierwszy składnik kolejnej operacji.

Podsumowanie
To wszystko, mamy już w pełni funkcjonalny kalkulator dla przeglądarek internetowych. Jak widzisz AngularJS jest prostym w użyciu (przynajmniej przy tworzeniu kalkulatora) i bardzo pomocnym frameworkiem dla aplikacji napisanych w JavaScript. Daje nam nowe możliwości wiązania stanu aplikacji z warstwą widoku opartą na html, co znacznie upraszcza tworzenie dynamicznych rozwiązań.

Pełny kod źródłowy tego kalkulatora jest dostępny na github.com.

niedziela, 30 czerwca 2013

Scala - Parametry nazwane i parametry domyślne a dziedziczenie

For English version of this article go here.

Podczas lektury Scala in Depth natknąłem się na interesujący problem związany z parametrami nazwanymi i dziedziczeniem klas. Co się stanie jeżeli w klasie potomnej nadpiszemy metodę i, przy okazji, zmienimy nazwy parametrów tej metody? Jak wpłynie to na sposób wywołania tej metody? A co w przypadku parametrów domyślnych, czy zachowają się podobnie do parametrów nazwanych? Okazuje się że nie, z tego powodu dziedziczenie parametrów domyślnych może skutkować powstaniem trudnych do wykrycia błędów w działaniu aplikacji.

Zacznę od podsumowania sposobów w jaki Scala umożliwia nam przekazywanie parametrów nazwanych oraz parametrów domyślnych do wywoływanych funkcji lub metod. Te funkcjonalności zostały wprowadzone od wersji 2.8 Scali.

Parametry pozycyjne (tak jak w Javie).
Przekazywanie parametrów do funkcji zwykle wykonuje się przez podanie właściwych parametrów w kolejności ich występowania w deklaracji funkcji. Przykład:

scala> def process(x1:Int, x2:Int) = x1 * 2 + x2

scala> process(2, 5)
res0: Int = 9

Parametry nazwane.
Jeżeli zależy nam na zmianie kolejności przekazywanych parametrów, lub na zwiększeniu czytelności wywołania funkcji, bądź przekazaniu tylko części parametrów (jeżeli metoda to dopuszcza), możemy wykorzystać parametry nazwane. Przykład:

scala> def process(x1:Int, x2:Int) = x1 * 2 + x2

// przekazanie parametrów bez zmiany kolejności
scala> process(x1 = 2, x2 = 5) 
res1: Int = 9 

// zmiana parametrów ze zmianą kolejności
scala> process(x2 = 5, x1 = 2) 
res2: Int = 9

Możliwe jest też wywoływanie funkcji łącząc parametry pozycyjne i parametry nazwane. Jednak warunkiem takiego postępowania jest to, aby parametry nazwane (o ile zmieniamy kolejność przekazywanych parametrów) były podawane po parametrach pozycyjnych. Przykład:

scala> process(2, x2 = 5) // tylko część parametrów z nazwą
res3: Int = 9 

// pierwszy parametr nazwany, ALE nie zmieniona kolejność parametrów
scala> process(x1 = 2, 5) 
res4: Int = 9 

// pierwszy parametr nazwany i zmiana kolejności parametrów
scala> process(x2 = 5, 2) 
<console>:9: error: positional after named argument.
              process(x2 = 5, 2)
                              ^

Parametry domyślne.
Jeżeli chcemy, aby użytkownik naszej funkcji nie musiał przekazywać wszystkich parametrów, możemy w deklaracji funkcji zdefiniować domyślną wartość jaką przybierze parametr, jeżeli nie zostanie przekazany podczas wywołania funkcji: Przykład:

scala> def sum(x1:Int, x2:Int, x3:Int = 0) = x1 + x2 + x3 
 
scala> sum(1, 2, 3)
res5: Int = 6 
 
scala> sum(1, 2)
res6: Int = 3

Co ciekawe, parametry z wartościami domyślnymi nie muszą występować na końcu listy parametrów. Jednak w tym przypadku jedyną możliwością wywołania funkcji bez podawania ich wartości jest wykorzystanie parametrów nazwanych:

scala> def sum(x1:Int, x2:Int = 0, x3:Int) = x1 + x2 + x3 
 
// tylko dwa parametry zostały przekazane
scala> sum(1, 3) 
<console>:9: error: not enough arguments for method sum: (x1: Int, x2: Int, x3: Int)Int.
Unspecified value parameter x3.
              sum(1, 3) 
 
// zdefiniowane parametry zostały przekazane po nazwie
scala> sum(x1 = 1, x3 = 3) 
res7: Int = 4


Parametry nazwane, a dziedziczenie klas.
Sprawdźmy teraz jak zachowają się parametry nazwane w przypadku dziedziczenia klas i nadpisywania metod. Jako przykład przyjmijmy następujące klasy:

class Calculator {
  def process(x1:Int, x2:Int) = x1 * 2 + x2
}

class BrokenCalculator extends Calculator {
  override def process(a:Int, b:Int) = super.process(a, b) + 1
}

Należy zwrócić uwagę, że nazwy parametrów metody process zostały zmienione z x1 i x2 na a i b.
Następnie stwórzmy ich instancje, ale w przypadku instancji BrokenCalculator przypiszmy je do referencji różnych typów:

val calculator = new Calculator
val brokenCalculator = new BrokenCalculator
val brokenCalculatorAsCalculator:Calculator = new BrokenCalculator

Czy przypisanie BrokenCalculator do referencji typu Calculator będzie miało wpływ na wynik działania metody? Okazuje się, że zależy to od sposobu przekazywania parametrów:

scala> calculator.process(2, 5)
res8: Int = 9
scala> brokenCalculator.process(2, 5)
res9: Int = 10
scala> brokenCalculatorAsCalculator.process(2, 5)
res10: Int = 10

W tym przypadku, gdy parametry przekazaliśmy pozycyjnie wszystkie wywołania zależą po prostu od klasy obiektu, dlatego brokenCalculator i brokenCalculatorAsCalculator zachowały się tak samo.
Co jeżeli zechcemy wykorzystać parametry nazwane?

scala> calculator.process(x2 = 5, x1 = 2)
res11: Int = 9 
 
// próba wywołania parametrów z nazwami z klasy Claculator
scala> brokenCalculator.sum(x2 = 5, x1 = 2)
<console>:11: error: value sum is not a member of BrokenCalculator
              brokenCalculator.sum(x2 = 5, x1 = 2)
                                       ^ 
 
scala> brokenCalculator.process(b = 5, a = 2)
res12: Int = 10 
 
// próba wywołania parametrów z nazwami z klasy BrokenCalculator
scala> brokenCalculatorAsCalculator.sum(b = 5, a = 2)
<console>:11: error: value sum is not a member of Calculator
              brokenCalculatorAsCalculator.sum(b = 5, a = 2)
                                           ^ 
 
scala> brokenCalculatorAsCalculator.process(x2 = 5, x1 = 2)
res13: Int = 10

Jak widać, w tym przypadku istotne staje się to, jakiego typu referencji używamy. W naszym przykładzie obiekty calculator oraz brokenCalculatorAsCalculator jako parametrów wymagały nazw x1x2, przy próbie przekazania nazw a i b wynikiem był błąd kompilacji. Analogicznie w przypadku obiektu brokenCalculator konieczne było przekazanie parametrów jako a i b, a przekazywanie x1 i x2 także spowodowało błąd kompilacji.

Wydaje mi się, że w tej sytuacji należy uświadomić sobie, że kontrakt  metody (czyli sposób w jaki wywołujemy metodę) jest determinowany przez rodzaj referencji którą wykorzystujemy, dzięki temu już kompilator może zweryfikować poprawność jej wywołania.

Parametry domyślne, a dziedziczenie klas.
Zmodyfikujmy teraz nasze klasy, definiując w ich metodach parametry domyślne. W tym przypadku BrokenCalculator zmienia wartość domyślną parametru x2:

class Calculator {
  def process(x1:Int, x2:Int = 0) = x1 * 2 + x2
}

class BrokenCalculator extends Calculator {
  override def process(x1:Int, x2:Int = 1) = super.process(x1, x2)
} 
Sprawdźmy jak będzie zmieniało się zachowanie metod w zależności od typu obiektu i typu referencji, którą wykorzystamy.

scala> calculator.process(2)
res14: Int = 4 // wartość domyślna 0
scala> brokenCalculator.process(2)
res15: Int = 5 // wartość domyślna 1
scala> brokenCalculatorAsCalculator.process(2)
res16: Int = 5 // wartość domyślna 1

W tym przypadku wyniki są zaskakujące (!), okazuje się, że jeżeli klasa potomna zmienia wartość domyślną prarametru to wartości domyślne parametrów nie zależą od typu referencji. Przy wywoływaniu brokenCalculatorAsCalculator do metody przypisano wartość domyślną 1, chociaż kontrakt z klasy Calculator deklaruje 0. Jak widzimy może to powodować problemy, zwłaszcza, że IDE (w moim przypadku IntelliJ IDEA) wskazuje, że wartością domyślną parametru x2 w wywołaniu brokenCalculatorAsCalculator będzie 0!

Sprawdźmy w takim razie jeszcze jeden przypadek. Co jeżeli klasa BrokenCalculator nie będzie definiowała wartości domyślnej dla parametru x2?

class BrokenCalculator extends Calculator {
  override def process(x1:Int, x2:Int) = super.process(x1, x2)
}

Czy możliwe jest w takiej sytuacji metody process przekazując tylko 1 parametr?

scala> calculator.process(2)
res17: Int = 4
scala> brokenCalculator.process(2)
res18: Int = 4
scala> brokenCalculatorAsCalculator.process(2)
res19: Int = 4

Okazuje się że tak! W tej sytuacji BrokenCalculator odziedziczył wartość domyślną nadawaną parametrowi x2, czyli jeżeli klasa potomna nie definiuje wartości domyślnej parametru, to dziedziczy ona wartość domyślną z klasy macierzystej.

Widać w tym przypadku sporą niekonsekwencję, w projektowaniu Scali. Co jeżeli jeden z programistów będzie korzystał z referencji typu Calculator. Weryfikując kontrakt referencji nie jest on w stanie stwierdzić jaką wartość domyślną przyjmie wywoływana metoda! Musi w tym celu sięgnąć do implementacji faktycznego obiektu jaki otrzyma... ale czasami nie jest możliwe ustalenie tego w czasie pisania kodu, prawda?

Chyba najlepszym sposobem zrozumienia jak działa dziedziczenie wartości domyślnych jest założenie, że klasa potomna zawsze dziedziczy definiowane wartości domyślne, ale może je w każdej chwili zmienić. A przy wywołaniu takich metod wartości domyślne są zależne od konkretnej implementacji klasy, a nie od typu referencji.

piątek, 28 czerwca 2013

Sortowanie list w Scala

Przyglądając się zmaganiom Jacka Laskowskiego z taskassin i programowaniem w Scali postanowiłem podsumować różne sposoby sortowania listy obiektów w Scali.

W przypadku Jacka do posortowania mamy listę zadań zdefinowanych następująco (uprościłem klasę na potrzeby tego wpisu):

case class Task(label: String, due: DateTime)
oraz mamy zdefiniowaną listę obiektów:
val tasks = List(Task("t1", DateTime.now),
                 Task("t2", DateTime.now + 2.hours), 
                 Task("t3", DateTime.now - 2.hours))
Wyjaśnienie: DateTime oraz .hours pochodzą z biblioteki nscala-time opakowującej JodaTime pod kątem Scali.

Na pierwszy rzut oka widzimy, że klasa List oferuje nam trzy metody służące do sortowania:
.sorted - do sortowania obiektów ze zdefiniowaną cechą Ordered
.sortBy - do sortowania obiektów po wybranym polu
.sortWith - do sortowania obiektów po podanej funkcji porównującej

Oczywiście, wybór którą z nich wykorzystamy zależy (lub powienien zależeć) od konteksu biznesowego w którym powstaje nasza aplikacja. Przyjżyjmy się poszczególnym sytuacjom, w których chcielibyśmy sortować obiekty klasy Task:



Przypadek 1. Jeżeli sortowanie zadań będzie zawsze odbywać się jedynie/głównie po polu due, to wykorzystajmy obiektowość i rozszerzmy Task o właściwość Ordered[Task]:

case class Task(label: String, due: DateTime) extends Ordered[Task] {
  def compare(other: Task): Int = this.due.compareTo(other.due)
}
dzięki temu sortowanie listy zadań będzie można wykonać następująco:
val sortedTasks = tasks.sorted
Trudno wyobrazić sobie coś prostrzego z punktu widzenia programisty wykorzystującego obiekty klasy Task.



Przypadek 2.1. Inaczej musimy postąpić, jeżeli przyjmiemy, że zadania można sortować na kilka sposobów, np. dodatkowo po nazwie. W takiej sytuacji można przekazać do metody sorted obiekt klasy Ordering[Task] który zdefiniuje nam sposób porządkowania elementów:

 val sortedTasks = tasks.sorted(new Ordering[Task] {
      def compare(a: Task, b: Task): Int = a.due.compareTo(b.due)
    })

można też wykorzystać metodę upraszczającą tworzenie obiektu Ordering:
val sortedTasks = tasks.sorted(Ordering.fromLessThan[Task](_.due < _.due))

Przypadek 2.2. Powyższe rozwiązanie nie wydaje się być bardzo eleganckie, ale można je wykorzystać do stworzenia wygodnego sposobu sortowania. W tym celu w obiekcie towarzyszącym możemy zdefiniować konkretne sposoby porządkowania wykorzystując Ordering[task]:
case class Task(label: String, due: DateTime)

object Task {
  //pełna składnia dofiniowania funkcji anonimowej
  val orderingByDue = Ordering.fromLessThan((a:Task, b:Task) => a.due < b.due)
  //skrócona notacja sposób definiowania funcji anonimowej
  val orderingByLabel = Ordering.fromLessThan[Task](_.label < _.label) 
}
W takiej sytuacji sortowanie wygląda następująco:
val tasksSortedByDue = tasks.sorted(Task.orderingByDue)
val tasksSortedByLabel = tasks.sorted(Task.orderingByLabel)



Przypadek 3. Jeżeli jednak nie chcemy narzucać programiście jakiego sortowania powinien używać można wykorzystać metodę sortBy, której parametr wskazuje, którego pola klasy należy użyć do sortowania (przez przekazanie settera dla danego pola):

val sortedTasks = tasks.sortBy((t:Task) => t.due)
A jeszcze lepiej użyć skróconej i bardziej czytelnej formy:
val sortedTasks = tasks.sortBy(_.due)
Dodatkowo bardzo zaletą sortBy jest możliwość zdefiniowania większej ilości pól po których ma być sortowana kolekcja (np. sortowanie osób najpierw po nazwisku, a jeżeli są takie same to po imeniu). W tym celu należy przekazać tuple zawierające pola które nas interesują:
val sortedTasks = tasks.sortBy((x) => (x.label, x.due))
W tym przypadku zadania zostaną posortowane według nazwy, a jeżeli kilka zadań będzie miało tę samą nazwę, to zostaną one posortowanie po czasie w ramach tej grupy.



Przypadek 4.1. Jeżeli rozwiązanie z punktu 3. nie jest wystarczające (bo. np sortowanie jest bardziej skomplikowane) metoda sortBy, która przyjmuje funkcję według której zostaną posortowane elementy, będzie jak znalazł:

val sortedTasks = tasks.sortWith((a:Task, b:Task) => a.due < b.due)
val sortedTasks = tasks.sortWith(_.due < _.due)
Przypadek 4.2. Należy także dodać, że w tym przypadku możliwe jest (podobnie jak w przypadku 2.) zdefiniowanie funkcji przekazywanych do sortWith jako elementów obiektu towarzyszącego:
object Task {
  val compareByDue = (a:Task, b:Task) => a.due < b.due
}
I wykorzystanie jej w ten sposób:
val sortedTasks = tasks.sortWith(Task.compareByDue)



Algorytm sortowania Przyglądając się sposobom sortowania list w Scali zacząłem zastanawiać się nad algorytmami wykorzystanymi do sortowania.

Sortowanie sekwencji przez wykorzystanie metod sorted, sortBy i sortWith wykorzystuje algorytm sortowania dostarczany przez JDK, a w szczególności metodę java.util.Arrays.sort(...). Scala przepisuje listę (lub dowolną sekwencję) do zwykłej tablicy i to na niej wykonywane jest sortowanie. Następnie na podstawie tej tablicy tworzona jest sekwencja odpowiedniego typu (np. List).

Metoda sortująca dostarczana przez JDK do sortowania tablicy obiektów wykorzystuje zmodyfikowany algorytm mergesort, który gwarantuje stabilność sortowania oraz złożoność obliczeniową O(n) = n log(n). Dodatkowo w przypadku gdy elementy są już częściowo posortowane wydajność tego algorytmu jest dużo większa.

Należy także zaznaczyć, że java.util.Arrays.sort(...) w przypadku tablic prymitywów wykorzystuje zoptymalizowany algorytm quicksort (inaczej niż w przypadku tablic obiektów, ponieważ w przypadku prymitywów nie musi być on stabilny), jednak aby móc wykorzystać tę możliwość z poziomu Scali należy wywołać tę metodę bezpośrednio, gdyż interfejsy Scali zawsze wywołają metodę dla tablicy obiektów.


Podsumowanie. Osobiście skłaniałbym się ku metodom 1. oraz 2.2. ponieważ umożliwiają zdefiniowanie kontekstu biznesowego, którego programista nie będzie musiał odkrywać za każdym razem, gdy będzie chciał posortować kolekcję (oczywiście mowa o bardzie skomplikowanych przypadkach niż ten w taskassin). Dodatkowo wydaje się że definiują właściwości klasy Task, a nie zestaw zachowań (jak to jest w przypadku pozostałych metod), które mogą jej dotyczyć.

Oczywiście zdaję sobie sprawę, że nie są to wszystkie możliwości sortowania list jakie oferuje Scala, ale myślę, że przedstawiłem te najbardziej użyteczne.