piątek, października 31, 2008

Czy polimorfizm w C# jest za słaby?

Polimorfizm jest bardzo przydatną cechą języków, która umożliwia skrócenie pisanego kodu, zwiększenie giętkości pisanych klas czy bibliotek, zwiększenie poziomu abstrakcji pisanego kodu - co w ostatecznym rozrachunku przekłada się na zmniejszenie nakładu pracy związanego z budową programu.

W C# mamy dostępne tzw. generics (link, link). Umożliwiają one np. tworzenie kontenerów dla obiektów dowolnego typu, podobnie jak wygląda to w C++:
[C#, 1]
Queue<int> kolejka_intow = new Queue<int>();

Wszystko wygląda fajnie, tylko że brakuje jednej rzeczy: możliwości "zaglądania" do obiektów polimorficznych. Nie możemy na przykład napisać funkcji:

public static T addGenerics<T>(T a, T b)
{
    return a + b;
}

Dlaczego? Kompilator słusznie wyrzuci nam:
Operator '+' cannot be applied to operands of type 'T' and 'T'

Z drugiej strony kod:

[C#, 2]
public static void testDoWithInstance()
{
     doWithInstance<int>(2);
     doWithInstance<float>(2);
     doWithInstance<string>("aaa");
     doWithInstance<int[]>(new int[] { 1, 2, 3 });
}

public static void doWithInstance<T>(T inst)
{
     System.Console.WriteLine(string.Format("<<{0}>>", inst));
}

Skompiluje się i wyświetli bez zarzutu:

<<2>>
<<2>>
<<aaa>>
<<System.Int32[]>>

Gdzie tkwi więc problem? Kod typu [2] skompiluje się, a w razie problemów (obiekt T nie może być przekształcony na napis) rzuci wyjątek. Oczywiście ktoś mógłby powiedzieć, że wszystko można jakoś wyświetlić, ale to nie jest sedno problemu. Sednem problemu jest to, że nie odkrycie czy dany obiekt obsługuje jakąś metodę odsuwamy do momentu wykonania programu. C# staje się więc w ten sposób efektywnie językiem dynamicznym.

Kod typu [1] natomiast wcale się nie będzie kompilować wcale. Kompilator nie wie z góry, czy obiekt typu T można dodać do drugiego obiektu typu T, innymi słowy czy obsługują one dodawanie. Można to obejść stosując mechanizm Reflection, który umożliwia analizowanie obiektów w trakcie wykonania programu. To rozwiązanie sprowadza się więc praktycznie do tego które mamy w [2], z tym, że nie możemy po prostu zrzutować danego obiektu na typ Object. Mechanizm Reflection ma dwie zasadnicze wady:
- jest dynamiczny, generuje wyjątki w trakcie działania programu, nie błędy kompilacji
- używanie go jest niewygodne i wymaga pisania dużych ilości dodatkowego kodu

Rozwiązaniem jest wprowadzenie kontekstów w postaci interfejsów do typów parametrycznych. Gwarantowałyby one implementację zadanych metod. Przykładowo:

[C#, 3]
public static T f<T implementing IPlus, IMinus>( T a, T b)
{
     return (a + a - b);
}
Z tego co się orientuje, nie ma jeszcze tego typu rozszerzenia w C#. A może się mylę?

sobota, października 25, 2008

Wywoływanie funkcji w C z języka C#

Udało mi się, po paru próbach, dojść do takiego kodu:

[C#]
       [DllImport("mytest.dll", EntryPoint="makeFFT")]
       private static extern void makeFFT_(IntPtr inputArray, IntPtr outputArray);

       public static void makeFFT(float[] inpArr, float[] outArr)
       {
            IntPtr inpArrBuf = Marshal.AllocHGlobal(sizeof(float) * inpArr.Length);
            IntPtr outArrBuf = Marshal.AllocHGlobal(sizeof(float) * outArr.Length);
            
            Marshal.Copy(inpArr, 0, inpArrBuf, inpArr.Length);

            makeFFT_(inpArrBuf, outArrBuf);

            Marshal.Copy(outArrBuf, outArr, 0, outArr.Length);
            
            Marshal.FreeHGlobal(inpArrBuf);
            Marshal.FreeHGlobal(outArrBuf);
       }

Gdzie funkcja w C ma typ:
[C]
void makeFFT( float* inArr, float* outArr )

Kod, choć niezbyt piękny, działa i robi to o co od niego oczekiwałem: wywołuje zawartą w bibliotece funkcję makeFFT gdzie inArr jest parametrem wejściowym, zaś outArr - wyjściowym. Zasadnicze pytanie brzmi: czy da się to zrobić krócej, bardziej elegancko?

niedziela, października 19, 2008

Prosty szablon wizualizacji

Wielu osobom wydaje się, że Haskell nie nadaje się do pisania czegokolwiek poza interpreterem innego języka :-) Nic bardziej mylnego! Jest to język ogólnego zastosowania, a przy tym jest on bardzo elegancki - znacznie bardziej niż ubogi w abstrakcje C czy zatłoczony składnią C++. No dobra, wystarczająco wkurzyłem fanów tych języków, czas zabrać się za konkrety ;-)

Zbudujemy dzisiaj w Haskellu prostą aplikację wykorzystującą OpenGL. Nie chcę tutaj pokazywać tajników programowania w OpenGL, od tego są inne strony. Będziemy potrzebować:


Zbudowanie ich wymaga odpowiednich plików nagłówkowych, ale to jest już mniej ciekawe zagadnienie.


  1. Nazwanie modułu

  2. Warto jest nazwać jakoś nasz program. Umieszczamy więc w pierwszej linii:
    >>> module Main where

  3. Importowanie odpowiednich bibliotek.

  4. Potrzebować będziemy OpenGL i GLFW (do robienia okienek i obsługi klawiatury):
    >>> import Graphics.Rendering.OpenGL -- lots of modules re-exported
    >>> import Graphics.UI.GLFW -- for window creation etc.

    Przydadzą się też biblioteka ułatwiająca programowanie wielowątkowe:
    >>> import Control.Concurrent
    Biblioteka do monad też się przyda:
    >>> import Control.Monad
    Z kolei ta biblioteka udostępnia funkcję printf, podobną do tej znanej z C:
    >>> import Text.Printf

  5. Inicjalizacja GLFW

  6. Początek funkcji main może wyglądać tak:
    >>> main = do
    >>>    initOk <- initialize -- [1]
    >>>    if not inikOk then error "Błąd inicjalizacji GLFW!" else return () -- [2]
    >>>     vmodes <- get videoModes -- [3]
    >>>     let bestVM = last vmodes -- [4]
    >>>     setWindowModeOk <- openWindow (videoModeToSize bestVM) [videoModeToRGBBits bestVM] FullScreen -- [5]
    >>>     if not setWindowModeOk then error "Nie udało się otworzyć okna" else return () -- [6]

    W linijce [1] próbujemy zainicjować GLFW. Funkcja ta zwraca wartość logiczną, którą sprawdzamy w [2], jeżeli nie udało się (ma ona wartość False) przerywamy działanie programu za pomocą funkcji error. Po pobraniu informacji o dostępnych rozdzielczościach ([3]) i wybraniu potencjalnie najlepszej z nich ([4]) robimy znów podobną rzecz: próbujemy otworzyć okno w tej rozdzielczości ([5]), jeżeli nam się nie powiedzie przerywamy działanie programu ([6]). Korzystamy przy tym z funkcji pomocniczych, o których opowiem za chwile.

    Niby działa, ale... można to napisać lepiej.

    >>> main = do
    >>>     tryTo initialize (error "Błąd inicjalizacji GLFW!")
    >>>     bestVM <- last `fmap` get videoModes
    >>>     tryTo (openWindow (videoModeToSize bestVM) [videoModeToRGBBits bestVM] FullScreen)
    >>>       (error "Nie udało się otworzyć okna")

    Co się zmieniło? Dwie rzeczy:
    - wprowadziliśmy funkcję tryTo
    - użyliśmy funkcji fmap
    Definicja funkcji tryTo jest prosta:

    >>> tryTo :: IO Bool -> IO () -> IO ()
    >>> tryTo doAction actionIfFailed = do
    >>>     b <- doAction
    >>>     if not b then actionIfFailed else return ()

    Przy pomocy funkcji when i rezygnując z niepotrzebnego cukru syntaktycznego można tą definicje istotnie skrócić:
    >>> tryTo doAction actionIfFailed = doAction >>= \b -> when (not b) actionIfFailed
    Nieco nieczytelnie można to zapisać jako:
    >>> tryTo doAction actionIfFailed = doAction >>= (flip unless) actionIfFailed
    Ale to już jest lekka przesada :-)

    Pozostaje jeszcze sprawa funkcji fmap. Pozwala ona nakarmienie funkcji argumentem pochodzącym z monady (tak na prawdę jej definicja jest nieco ogólniejsza, ale to nieistotne). Możliwa jej definicja w kontekście monad jest taka:
    >>> fmap pureFunc monadAction = monadAction >>= \ v -> return (pureFunc v)

    Dzięki tym dwóm funkcjom kod został skrócony i stał się bardziej przejrzysty. Przejdźmy więc nieco dalej.

  7. Funkcje pomocnicze

  8. W kodzie powyżej wykorzystałem dwie funkcje o następujących definicjach:
    videoModeToSize vm = Size (fromIntegral $ videoWidth vm) (fromIntegral $ videoHeight vm)
    videoModeToRGBBits vm = DisplayRGBBits (videoRedBits vm) (videoGreenBits vm) (videoBlueBits vm)
    Są to swego rodzaju funkcje "rzutowania", czy też "ekstrakcji": z trybu wideo (vm :: VideoMode) otrzymujemy za pomocą pierwszej funkcji rozmiar okna, zaś za pomocą drugiej - liczbę bitów poszczególnych kolorów. Nie jest to parametr, który koniecznie musimy podać, jednak pozwala on wymusić pewne zachowanie.

  9. Dalsze ustawianie środowiska

  10. Skoro mamy już okno, warto by nadać mu tytuł
    >>> windowTitle $= "My first Haskell + OpenGL app"
    Co się wydażyło w tej linijce?
    Skorzystaliśmy z funkcji $= do ustawienia parametru windowTitle. Jest to funkcja implementowana przez typy należące do klasy HasSetter. W większości popularnych języków programowania (C++, C#, Delphi, Ruby...) mamy możliwość tworzenia parametrów, choć różnie się nazywa w tych językach te mechanizmy. Zasadniczo chodzi o pewien parametr klasy, który można ustawić na pewną wartość. W momencie wykonania przypisania nie jest jednak wprost zapisywana w pamięci pewna wartość, lecz wykonywana jest odpowiednia metoda klasy z argumentem będącym wartością "przypisywaną" parametrowi. Jest to mechanizm wygodny i użyteczny, o czym świadczy chociażby fakt szerokiego wsparcia w językach. W Haskellu możemy ten mechanizm zaimplementować sami i - co więcej - jest to bardzo proste. Po szczegóły odsyłam do źródeł.

    W podobny sposób ustawimy callbacki (brrr co za słowo) do reagowania na zdarzenia środowiska:

    >>> keyCallback $= myKeyCallback
    >>> charCallback $= myCharCallback

    Gdzie myKeyCallback i myCharCallback to funkcje:

    >>> -- wywoływany przy naciśnięcu jakiegokolwiek klawisza
    >>> myKeyCallback key Press = do
    >>>    case key of
    >>>      SpecialKey ENTER -> restoreWindow
    >>>      SpecialKey ESC -> iconifyWindow
    >>>      _ -> return ()
    >>> myKeyCallback _ _ _ = return ()

    >>> -- wywoływany przy naciśnięcia klawisza, któremu odpowiada jakiś znak
    >>> myCharCallback chr st = putStrLn (printf "Znak '%c' został %s." chr (if st == Press then "naciśnięty" else "puszczony"))

    myKeyCallback reaguje na naciśnięcie entera i klawisza escape: jedno z nich chowa okno, drugi je pokazuje. Z kolei myCharCallback wypisuje informacje o tym, że ktoś nacisnął lub puścił jakiś klawisz któremu odpowiada pewien znak. Można zawrzeć funkcjonalność myCharCallback w myKeyCallback, ale nie byłoby to wygodne.

    Zmienimy jeszcze rozmiar punktu:
    >>> pointSize $= 4.0

    Nieco inaczej wygląda ustawienie dwóch innych parametrów:

    >>> enableSpecial KeyRepeat -- powtarzanie klawiszy
    >>> enableSpecial MouseCursor -- widoczny kursor myszy
    >>> --enableSpecial AutoPollEvent -- automatyczne pobieranie nowych eventów [*]

    Linia [*] jest wykomentowana, gdyż nie chcemy tak na prawdę tej funkcjonalności od GLFW. Jeżeli jest ona włączona to eventy pobierane są automatycznie przy wywołaniu funkcji swapBuffers. Ponieważ czasami wyświetlamy niewiele klatek na sekundę, nasza aplikacja zachowywałaby się w takich momentach nieprzyjemnie. Zamiast tego uruchomimy w tym celu oddzielny wątek:

    >>> forkIO (forever pollEvents)

    Zostanie on automatycznie zabity przy końcu działania programu.

    Chcielibyśmy też ustawić jakieś opcje widoku. Nie będziemy wymyślni, wystarczy nam ortogonalny widok "2D":

    >>> let Size sizeX sizeY = videoModeToSize bestVM
    >>> ortho2D 0 (fromIntegral sizeX) 0 (fromIntegral sizeY)

    Użycie funckji fromIntegral jest w paru miejscach konieczne, by zmienić typ z jednego całkowitoliczbowego na jakiś inny typ liczbowy (np. Int -> Double, GLint -> Int).

  11. Główna pętla renderingu

  12. Jesteśmy teraz gotowi na wywołanie pętli rysowania. Robi się to tak:
    >>> mainLoop
    ;-)

    Oczywiście musimy podać definicję mainLoop:
    >>> mainLoop :: IO ()
    >>> mainLoop = do
    >>>     (Position px py) <- get mousePos
    >>>     clear [ColorBuffer]
    >>>     let gray = Color3 0.5 0.5 0.5 :: Color3 Float
    >>>     let myPoint = (Vertex2 (fromIntegral px) (fromIntegral py)) :: Vertex2 Float
    >>>     renderPrimitive Points $ color gray >> vertex myPoint
    >>>     swapBuffers
    >>>     mainLoop

    Rysujemy więc w aktualnym położeniu kursora duży (ustawiliśmy to przed chwilą) szary punkt.

  13. A jak stąd wyjść?

  14. Wszystko wygląda fajnie, ale czasami chcemy wyjść z programu, prawda? Aby to zrealizować wykorzystamy pewien rodzaj zmiennych - MVar - których obsługę znajdziemy w module Control.Concurrent (a dokładniej tutaj).

    Obiekt typu MVar Int jest "pudełkiem" które może być albo puste, albo przechowywać obiekt typu Int. Stworzymy więc nową, pustą zmienną MVar która gdy zostanie zapełniona będzie sygnalizować konieczność zakończenia aplikacji. Proste, prawda?

    Do funkcji main dopisujemy przed wywołaniem mainLoop jedną linijkę:
    >>> quitMVar <- newEmptyMVar :: IO (MVar ())
    Teraz quitMVar będzie pudełkiem w którym może znaleść się wartość typu () - jest to typ o jednym elemencie, który także wygląda jak () - jest to krotka o zerze elementów:
    ()
    -- nie ma krotek jednoelementowych: (1)
    (1,2)
    (1,2,3)
    (1,2,3,4)
    (1,2,3,4,5)

    Nie potrzebujemy zatrudniać tutaj typu Bool czy też Int, więc nie robimy tego.

    quitMVar przekazujemy teraz do zmodyfikowanych wersji myKeyCallback i mainLoop:

    >>> keyCallback $= myKeyCallback quitMVar

    >>> mainLoop quitMVar

    Definicje te wyglądają tak:

    >>> -- wywoływany przy naciśnięcu jakiegokolwiek klawisza
    >>> myKeyCallback quitter key Press = do
    >>>    case key of
    >>>      SpecialKey BACKSPACE -> iconifyWindow
    >>>      SpecialKey ENTER -> restoreWindow
    >>>      SpecialKey ESC -> putMVar quitter () -- umieszczamy wartość w "pudełku"
    >>>      _ -> return ()
    >>> myKeyCallback _ _ _ = return ()

    >>> mainLoop :: MVar () -> IO ()
    >>> mainLoop qMV = do
    >>>      (Position px py) <- get mousePos
    >>>      clear [ColorBuffer]
    >>>      let gray = Color3 0.5 0.5 0.5 :: Color3 Float
    >>>      let myPoint = (Vertex2 (fromIntegral px) (fromIntegral py)) :: Vertex2 Float
    >>>      renderPrimitive Points $ color gray >> vertex myPoint
    >>>      swapBuffers
    >>>      tryTo (not `fmap` isEmptyMVar qMV) (mainLoop qMV) -- jeżeli qMV jest pusta, to kontynuujemy

  15. Sprzątamy po sobie...


  16. Kiedy wyszliśmy już z pętli renderingu nie zostało wiele do zrobienia. Zamykamy okno i idziemy spać:

    >>> closeWindow


Choć post ten wyszedł na znacznie dłuższy niż początkowo zamierzałem, sam kod jest zwarty i niewielki. Można go obejrzeć tutaj i tutaj. Kod źródłowy dostępny do ściągnięcia tutaj.

Mam nadzieję, że post wyszedł choć trochę ciekawie :-)

Zadania dla czytelników:
Znaleźć błąd w programie - łatwo go zauważyć po jego uruchomieniu

sobota, października 18, 2008

Haskell + OpenGL

Krótki szablon jak w Haskellu zapisać zrzut ekranu renderowanego przez OpenGL.
Plik zapisywany jest do formatu PAM, można go potem przekonwertować do dowolnego innego formatu rastrowego, np. za pomocą narzędzia convert z pakietu ImageMagick. Można też skorzystać z pakietu Netpbm.

Link na hpaste.org
Link na moim serwerku