poniedziałek, 9 stycznia 2012

Programowanie deklaratywne vs programowanie imperatywne

    Na wstępie skrótowo wytłumaczę, co rozumiem pod pojęciami zawartymi w tytule. Programowanie imperatywne polega na tym, że kod programu składa się z instrukcji (czyli rozkazów). Najbardziej typowe instrukcje to przypisania, wywołania procedur / metod, instrukcje warunkowe czy pętle. W programowaniu imperatywnym zmieniający się stan programu jest czymś naturalnym, a niedeterministyczne działanie procedur i metod - czymś często występującym.
    Zupełnie innym podejściem jest programowanie deklaratywne. W tym przypadku nie pisze się, co po kolei program ma robić, tylko jaką wartość chce się uzyskać. Czyli program deklaratywny nie jest zbiorem instrukcji, tylko zbiorem deklaracji właściwości obiektu, który chcemy uzyskać na wyjściu.
    Wśród powszechnie używanych języków generalnego zastosowania jest niewiele języków czysto deklaratywnych (czyli takich, w których nie ma instrukcji ani skutków ubocznych). Jedynym, jaki ja znam jest Haskell. Niemniej jednak jest cała masa języków prawie :-) deklaratywnych, które preferują deklaracje wartości nad instrukcjami, ale pozostawiają w swojej składni instrukcje np. żeby uprościć operacje I/O. Do takich języków należy cała "rodzina" ML (czyli m.in. OCaml i F#) a także kompilowana do JVM Scala.
    Język C# był na początku czysto imperatywny. Na szczęście, w każdej kolejnej wersji nabierał coraz więcej możliwości programowania funkcjonalnego (czyli deklaratywnego). Programując w najnowszej wersji (C# 4.0) często stoimy przed wyborem, czy zaimplementować coś imperatywnie, czy deklaratywnie.
    Jakiś czas temu zastanawiałem się, jak te dwa style przekładają się na wydajność programu i postanowiłem trochę poeksperymentować...
    Postanowiłem zaimplementować w miarę prosty algorytm na dwa sposoby, a następnie porównać czasy wykonania. Do implementacji wybrałem funkcję, która bierze na wejściu dwie listy i zwraca informację, czy druga lista jest podzbiorem pierwszej.
    Najprostsza implementacja imperatywna byłaby następująca:

public bool IsSubset<T>(List<T> first, List<T> second)
{
    foreach(T elem in second)
    {
        if(!first.Contains(elem))
            return false;
    }
    return true;
}

    Powyższe rozwiązanie ma złożoność kwadratową (a w zasadzie: prostokątną :-) ). Jeśli pierwsza lista będzie miała 100 000 elementów, a druga będzie jej 10 000-elementowym podzbiorem, to na moim komputerze taka metoda wykona się w czasie ok. 39 sekund. Typowy, średnio lubiący swoją pracę programista nawet nie będzie się zastanawiał, czy można to napisać lepiej, bo:
  1. "terminy gonią",
  2. "nie chce mi się",
  3. "czas procesora jest tańszy niż czas programisty",
  4. ...
    Trochę lepszy programista pomyśli, że da się napisać implementację działającą w czasie n * log n:
public bool IsSubsetBinarySearch<T>(List<T> first, List<T> second) 
    where T: IComparable<T>
{
    var array = new T[first.Count];
    first.CopyTo(array);
    Array.Sort(array);
    foreach (var elem in second) {
        if(Array.BinarySearch(array, elem) < 0)
            return false;
    }
    return true;            
}

Powyższe rozwiązanie dla tych samych danych "wypluje" wynik po czasie ok. 3,14 sekund. Mało kto szukałby w takiej sytuacji lepszego algorytmu, bo:
    a.  "terminy gonią",
    b.   ...
    z.  "klientowi nic się nie stanie, jak poczeka kilka sekund"
    Nie jest to jednak rozwiązanie optymalne, bo istnieje lepsza, liniowa implementacja:
public bool IsSubsetHashSet<T>(List<T> first, List<T> second)
{
    var hs = new HashSet<T>(first);
    foreach (var elem in second) 
    {
        if(!hs.Contains(elem))
            return false;
    }
    return true;
}

    Kod, który znajduje się powyżej to są już granice moich imperatywnych zdolności :-) Zaimplementowana w taki sposób metoda zakończyła działanie po 0,047 sekundy.
    Teraz przejdźmy do wersji deklaratywnej (używającej Linq to Objects):
public bool IsSubsetLinq<T>(List<T> first, List<T> second)
{
    return second
        .Except(first)
        .Count() == 0;
}

    Powyższa metoda jest wręcz matematyczną definicją problemu, jaki sobie postawiłem. Jest krótsza, prostsza i czytelniejsza nawet od najbardziej naiwnej wersji imperatywnej. A działa zaskakująco dobrze: kończy działanie po 0,063 sekundy! Co prawda, jest to troszkę więcej niż w przypadku optymalnej implementacji imperatywnej, ale:
  • wygląda na to, że asymptotyczny czas działania jest taki sam (nieznaczna różnica),
  • czas poświęcony na napisanie kodu był bardzo krótki,
  • taki kod jest na tyle prosty, że może go napisać nawet słaby programista.
    Ten ostatni argument jest według mnie, szczególnie istotny. Programowanie funkcyjne jest, po przełamaniu początkowych trudności, po prostu łatwiejsze. Jeśli z niego zrezygnujemy na rzecz wyżyłowanych imperatywnych implementacji, to się chyba przejedziemy... Praktyka pokazuje (takie mam wrażenie), że łatwiej jest znaleźć dziewicę w akademiku niż optymalną implementację w dużym, imperatywnie napisanym systemie informatycznym :-)

poniedziałek, 2 stycznia 2012

Darmowe IDE do F#

    Ostatnio szukałem narzędzia, które pozwoli mi programować w domu w F#. Oczywiście, nie chciałem płacić za żadną licencję, ani tym bardziej pisać w kodu w notatniku (interesowało mnie IDE). Na razie w ofercie Microsoftu nie ma czegoś takiego, jak Visual F# Express Edition (pomimo, że F# jest w tej chwili jednym z trzech, obok C# i VB, głównych języków .NETa), więc postanowiłem sprawdzić inne darmowe środowiska.
    Na pierwszy ogień poszedł SharpDevelop. Środowisko to rzeczywiście pozwala na tworzenie projektów F#, ale nie oferuje żadnych narzędzi, do których przyzwyczajony jest każdy programista. Nie ma ani podpowiadania składni, ani dokumentacji funkcji po najechaniu na nią myszką, ani żadnych innych ułatwień. Jedyne, co mamy, to edytor kodu z (zawierającym bugi) kolorowaniem składni oraz integracja z kompilatorem. Według mnie, to zdecydowanie za mało, jak na IDE.
    Później znalazłem informacje, że MonoDevelop ma wtyczkę do F#, która wspiera podpowiadanie składni. Niestety, nie udało mi się jej zainstalować (a próbowałem na wszelkie możliwe sposoby). Z tego, co zdążyłem się zorientować wynika, że do najnowszej windowsowej wersji MonoDevelop wdarł się bug uniemożliwiający instalację wyżej wymienionej wtyczki.
    W zaistniałej sytuacji spróbowałem trzeciej opcji, która polegała na zainstalowaniu Visual Studio 2010 Shell, czyli czystego VS bez wsparcia dla żadnego języka, a następnie zainstalowaniu F#. Po tym zabiegu VS zaczął wspierać F# w pełnym zakresie (identycznie, jak w płatnej wersji), czyli osiągnąłem swój cel :-)
    Podobno można również programować w F# przy użyciu Emacsa, ale tej opcji nawet nie sprawdzałem, bo nie jestem przyzwyczajony do tego środowiska...