Wbrew pozorom łatwo napisać taką instancję. Nie będzie ona spełniała warunku
> (read . show) == id
ale może być użyteczna. Można ją znaleźć w przypadku GHC 6.10 w module Text.Show.Functions. Wygląda ona tak:
instance Show (a -> b) where
showsPrec _ _ = showString "<function>"
Dzisiaj wpadłem na nieco pożyteczniejszą formę. Niestety, wszystko ma swoją cenę - zadziała ona tylko dla funkcji których argumenty są elementami klasy Typeable.
instance (Typeable a, Typeable b) => Show (a -> b) where
show _ = "<function: " ++ show (typeOf (undefined :: a)) ++ " -> " ++ show (typeOf (undefined :: b)) ++ ">"
Przykładowe działanie:
*Main> print print
Loading package syb ... linking ... done.
<function: () -> IO ()>
*Main> :set -Wall
*Main> print print
<interactive>:1:6:
Warning: Defaulting the following constraint(s) to type `()'
`Show a' arising from a use of `print' at <interactive>:1:6-10
`Typeable a' arising from a use of `print' at <interactive>:1:0-10
In the first argument of `print', namely `print'
In a stmt of a 'do' expression: it <- print print
<function: () -> IO ()>
*Main> print putStrLn
<function: [Char] -> IO ()>
*Main> print id
<interactive>:1:0:
Ambiguous type variable `a' in the constraint:
`Typeable a' arising from a use of `print' at <interactive>:1:0-7
Probable fix: add a type signature that fixes these type variable(s)
*Main> print (id :: Int -> Int)
<function: Int -> Int>
*Main> print (undefined :: Int -> Int -> Int)
<function: Int -> Int -> Int>
Pokazywanie postów oznaczonych etykietą haskell. Pokaż wszystkie posty
Pokazywanie postów oznaczonych etykietą haskell. Pokaż wszystkie posty
niedziela, lutego 22, 2009
wtorek, lutego 03, 2009
Przetestuj, jak TWOJA przeglądarka renderuje UTF-8
A tak na prawdę to tylko krótki programik do wypisania wszystkich znaków rozpoznawanych jako litery w kodowaniu UTF-8.
> module Main where
> import Prelude hiding ( writeFile )
> import System.IO.UTF8 ( writeFile )-- package 'utf8-string' on Hackage
> import Data.List.Split ( splitEvery ) -- package 'split' on Hackage
> import Data.Char ( isAlpha )
> import Data.List ( intercalate )
> main = writeFile "out.html" (header ++ chars ++ footer)
> where
> header = "<HTML><HEAD><META HTTP-EQUIV=\"Content-type\" CONTENT=\"text/html; charset=utf-8\"></HEAD><BODY>"
> chars = intercalate "\n<br>" . splitEvery 40 . filter isAlpha $ [ minBound .. maxBound ]
> footer = "</BODY></HTML>"
Przykładowy wynik działania znajduje się tutaj. Treść tego posta jest jednocześnie kodem programu - wystarczy zaznaczyć całość, skopiować i zapisać z rozszerzeniem .lhs. Zrozumie go GHC, choć do uruchomienia będą potrzebne dodatkowe pakiety (patrz kod programu).
> module Main where
> import Prelude hiding ( writeFile )
> import System.IO.UTF8 ( writeFile )-- package 'utf8-string' on Hackage
> import Data.List.Split ( splitEvery ) -- package 'split' on Hackage
> import Data.Char ( isAlpha )
> import Data.List ( intercalate )
> main = writeFile "out.html" (header ++ chars ++ footer)
> where
> header = "<HTML><HEAD><META HTTP-EQUIV=\"Content-type\" CONTENT=\"text/html; charset=utf-8\"></HEAD><BODY>"
> chars = intercalate "\n<br>" . splitEvery 40 . filter isAlpha $ [ minBound .. maxBound ]
> footer = "</BODY></HTML>"
Przykładowy wynik działania znajduje się tutaj. Treść tego posta jest jednocześnie kodem programu - wystarczy zaznaczyć całość, skopiować i zapisać z rozszerzeniem .lhs. Zrozumie go GHC, choć do uruchomienia będą potrzebne dodatkowe pakiety (patrz kod programu).
piątek, stycznia 30, 2009
Ciąg Fibonacciego
Chyba najkrótsza definicja jaką widziałem:
> import Control.Monad.Fix
> fibs :: [Integer]
> fibs = fix ((0:) . scanl (+) 1)
Haskell FTW!
> import Control.Monad.Fix
> fibs :: [Integer]
> fibs = fix ((0:) . scanl (+) 1)
Haskell FTW!
środa, stycznia 07, 2009
Zabawy z Twitterem i GDB
Dzisiaj miałem potrzebę uruchomić serwer gry Killer Mud na debuggerze gdb: wczorajsze zmiany w kodzie spowodowały, że w ciągu doby serwer 3 razy się wywalił. Ponieważ chcę mieć możliwość debugowania kodu w przypadku jakichkolwiek problemów proces zostanie zamrożony. Sesja GDB uruchomiona jest spod screen'a, dzięki czemu nie muszę być zalogowany na serwerze by program mimo wszystko działał.
Tutaj jednak pojawia się problem: skąd mam wiedzieć, że program ciągle działa? Chciałbym dostawać jakieś powiadomienie w razie wystąpienia problemu. Poszperałem w paru źródłach, założyłem konto na Twitterze i ostatecznie stworzyłem taki oto plik .gdbinit:
Jak można się domyśleć, o zatrzymaniu muda wysyłane są powiadomienia na Twittera, podobnie o kontynuacji jego uruchomienia. Przy zatrzymaniu wysyłany jest także mail.
Stworzenie sesji jest dość proste:
Teraz robimy tylko "Ctrl-a d" i mamy działającą w tle sesję GDB.
Garść linków:
Tutaj jednak pojawia się problem: skąd mam wiedzieć, że program ciągle działa? Chciałbym dostawać jakieś powiadomienie w razie wystąpienia problemu. Poszperałem w paru źródłach, założyłem konto na Twitterze i ostatecznie stworzyłem taki oto plik .gdbinit:
define hook-run
set logging overwrite off
set logging file killer-gdb-log.txt
set logging on
end
define hook-stop
backtrace
shell rm -rf hook-stop-log.txt
shell tail -n 50 killer-gdb-log.txt >> hook-stop-log.txt
shell echo "----------------------" >> hook-stop-log.txt
shell tail -n 50 killer-run-log.txt >> hook-stop-log.txt
shell tar -czf killer-gdb-log.txt.tgz killer-gdb-log.txt
shell tar -czf killer-run-log.txt.tgz killer-run-log.txt
shell echo | mail -a killer-gdb-log.txt.tgz -a killer-run-log.txt.tgz -q "hook-stop-log.txt" -s "killer :: gdb :: hook-stop" ADRES@MAILOWY.COM
shell ~/tener/twidge update "killer :: gdb :: hook-stop"
end
define hook-continue
shell ~/tener/twidge update "killer :: gdb :: hook-continue"
end
Jak można się domyśleć, o zatrzymaniu muda wysyłane są powiadomienia na Twittera, podobnie o kontynuacji jego uruchomienia. Przy zatrzymaniu wysyłany jest także mail.
Stworzenie sesji jest dość proste:
screen
cd ~/mud/area
gdb ../src/rom
run 3000 >> killer-run-log.txt 2>&1
Teraz robimy tylko "Ctrl-a d" i mamy działającą w tle sesję GDB.
Garść linków:
- Screen: http://en.wikipedia.org/wiki/GNU_Screen
- GDB, manual: http://www.linuxtopia.org/online_books/redhat_linux_debugging_with_gdb/index.html
- Twitter: http://twitter.com
- Twidge, klient Twittera napisany w Haskellu: http://software.complete.org/software/projects/show/twidge
- moje konto na Twitterze: http://twitter.com/tener
- konto muda na Twitterze: http://twitter.com/killermud
niedziela, listopada 23, 2008
Pierwsza wersja wikiTranslate - 0.0
Posiedziałem dzisiaj nieco nad wikiTranslate i stworzyłem jego pierwszą, testową wersję. Nie ma tego dużo - "wc -l *.hs" mówi o 44 linijkach. A tak na prawdę istotnych jest 16 z nich. Reszta to importy oraz moduł pomocniczy - pakiet "download-curl" nie chce się instalować na Windowsach więc napisałem mikro moduł który może go do pewnego stopnia zastąpić.
Wymagane pakiety: utf8-string, xml-light, tagsoup.
Zalecane: download-curl.
Ściągnąć z tej strony.
Krótka instrukcja.
1. Ściągamy i rozpakowujemy (wt.7z zawiera archiwa dla Linuxa i Windowsa)
2. Uruchamiamy program w konsoli podając jako parametry słowa do przetłumaczenia
3. Program wypisuje nam tłumaczenie na ekran
Wszystko działa fajnie tylko... pewne rzeczy są na stałe zaszyte w programie:
- język źródłowy: domyślnie polski ("pl"). Patrz "baseLanguge" w pliku wt.hs
- języki docelowe: ustawione na sztywno są nastepujące języki:
> filterLang = ["en","it","pt","de"]
Aby zmienić te stałe należy wyedytować plik wt.hs i zrekompilować program. Zakładam znajomość ghc - to jest wersja alpha więc nie osoby do których ma trafić ten post powinny potrafić sobie z tym poradzić :-)
TODO w kolejności priorytetów:
- wczytywanie języka źródłowego i języków docelowych z linii poleceń
- lepsze radzenie sobie z UTF8 - rosyjski wychodzi beznadziejnie
- WebUI
- radzenie sobie ze stronami "disambiguation"
Na deser wynik przykładowego zapytania
--- cut ---
[tener@tenserwer wikiTranslate]$ ./wt "Algorytm A*" dąb wikipedia polska wrocław "piłka nożna" "macierz odwracalna" "rachunek lambda"
Translations for term "Algorytm A*" in language "pl"
de A*-Algorithmus
en A* search algorithm
it A*
pt Algoritmo A*
------------------------------
Translations for term "dÂąb" in language "pl"
de Eichen
en Oak
it Quercus
pt Carvalho
------------------------------
Translations for term "wikipedia" in language "pl"
de Wikipedia
en Wikipedia
it Wikipedia
pt WikipĂŠdia
------------------------------
Translations for term "polska" in language "pl"
de Polen
en Poland
it Polonia
pt PolĂÂłnia
------------------------------
Translations for term "wrocÂław" in language "pl"
de Breslau
en WrocĂ
Âaw
it Breslavia
pt WrocĂ
Âaw
------------------------------
Translations for term "piÂłka noÂżna" in language "pl"
de FuĂÂball
en Association football
it Calcio (sport)
pt Futebol
------------------------------
Translations for term "macierz odwracalna" in language "pl"
de Reguläre Matrix
en Invertible matrix
it Matrice invertibile
pt Matriz inversa
------------------------------
Translations for term "rachunek lambda" in language "pl"
de Lambda-KalkĂÂźl
en Lambda calculus
it Lambda calcolo
pt CĂÂĄlculo lambda
------------------------------
--- cut ---
Wymagane pakiety: utf8-string, xml-light, tagsoup.
Zalecane: download-curl.
Ściągnąć z tej strony.
Krótka instrukcja.
1. Ściągamy i rozpakowujemy (wt.7z zawiera archiwa dla Linuxa i Windowsa)
2. Uruchamiamy program w konsoli podając jako parametry słowa do przetłumaczenia
3. Program wypisuje nam tłumaczenie na ekran
Wszystko działa fajnie tylko... pewne rzeczy są na stałe zaszyte w programie:
- język źródłowy: domyślnie polski ("pl"). Patrz "baseLanguge" w pliku wt.hs
- języki docelowe: ustawione na sztywno są nastepujące języki:
> filterLang = ["en","it","pt","de"]
Aby zmienić te stałe należy wyedytować plik wt.hs i zrekompilować program. Zakładam znajomość ghc - to jest wersja alpha więc nie osoby do których ma trafić ten post powinny potrafić sobie z tym poradzić :-)
TODO w kolejności priorytetów:
- wczytywanie języka źródłowego i języków docelowych z linii poleceń
- lepsze radzenie sobie z UTF8 - rosyjski wychodzi beznadziejnie
- WebUI
- radzenie sobie ze stronami "disambiguation"
Na deser wynik przykładowego zapytania
--- cut ---
[tener@tenserwer wikiTranslate]$ ./wt "Algorytm A*" dąb wikipedia polska wrocław "piłka nożna" "macierz odwracalna" "rachunek lambda"
Translations for term "Algorytm A*" in language "pl"
de A*-Algorithmus
en A* search algorithm
it A*
pt Algoritmo A*
------------------------------
Translations for term "dÂąb" in language "pl"
de Eichen
en Oak
it Quercus
pt Carvalho
------------------------------
Translations for term "wikipedia" in language "pl"
de Wikipedia
en Wikipedia
it Wikipedia
pt WikipĂŠdia
------------------------------
Translations for term "polska" in language "pl"
de Polen
en Poland
it Polonia
pt PolĂÂłnia
------------------------------
Translations for term "wrocÂław" in language "pl"
de Breslau
en WrocĂ
Âaw
it Breslavia
pt WrocĂ
Âaw
------------------------------
Translations for term "piÂłka noÂżna" in language "pl"
de FuĂÂball
en Association football
it Calcio (sport)
pt Futebol
------------------------------
Translations for term "macierz odwracalna" in language "pl"
de Reguläre Matrix
en Invertible matrix
it Matrice invertibile
pt Matriz inversa
------------------------------
Translations for term "rachunek lambda" in language "pl"
de Lambda-KalkĂÂźl
en Lambda calculus
it Lambda calcolo
pt CĂÂĄlculo lambda
------------------------------
--- cut ---
wtorek, listopada 11, 2008
I gdzie jest błąd?
Ostatnio na liście mailingowej haskell-cafe kilka razy padło pytanie o niedziałający z jakiegoś powodu program. W każdym z tych okazało się że problem leżał po stronie programisty :-)
Zadanie jest proste: uruchamiamy pewien proces, karmimy go danymi i oczekujemy od niego odpowiedzi. Naiwny kod wygląda tak:
> (p_stdin, p_stdout, p_stderr, p_handle) <- runInteractiveCommand jakas_komenda
> hPut p_stdin jakies_dane
> odpowiedz <- hGet p_stdout
Powyższy kod może, ale nie musi zadziałać. Co więcej: może raz działać dla danego programu, a raz nie działać. Prowadzi to do trudnych do debugowania i przez to irytujących błędów. Niestety: problem wynika z nieznajomości semantyki kanałów (ang. pipe - moje tłumaczenie jest chałupnicze) pomiędzy procesami.
Połączenia te mają ograniczoną pojemność "bufora". Po jego zapełnieniu proces który próbuje go "przepełnić" jest usypiany, aż bufor zostanie nieco opróżniony. Jest to (całkowicie słuszny) środek zaradczy przeciwko zużyciu przez nadgorliwy proces wszelkich zasobów systemu - w przeciwnym wypadku system musiałby przechować dowolnie dużo danych które zapisał dany proces. Bufor ten może być dość niewielki, np. 300 kb.
Jaki to ma związek z powyższym kodem? Ano taki, że w wyniku takiego właśnie działania kanałów kod ten powoduje często deadlocks - zakleszczenia.
Oto co się dzieje.
Uruchamiamy wysyłanie danych do naszego procesu:
> hPut p_stdin jakies_dane
Ale ów proces nie konsumuje ich na raz w całości. Zamiast tego zaczyna wysyłać częściowe porcje danych które wędrują do p_stdout. Wysyła ich na tyle dużo, że bufor p_stdout zapełnia się. Zostaje więc uśpiony. Aby został obudzony musimy odebrać z p_stdout porcję danych. Ale nie możemy tego zrobić - jeszcze nie skończyliśmy wysyłać mu danych na p_stdin!
Rozwiązanie jest proste: należy uruchomić wysyłanie danych w innym wątku:
> forkIO (hPut p_stdin jakies_dane)
W tym momencie jeden wątek będzie realizował wysyłanie danych, a drugi odbieranie. Co prawda jeden z nich może zostać uśpiony (bo np. proces nie odebrał jeszcze wszystkich danych i wykonuje teraz jakieś obliczenia) ale nie spowoduje to zakleszczenia.
Scenariusz może się jeszcze bardziej skomplikować, jeżeli interesuje nas równocześnie wyjście z p_stderr. W tym momencie ten kod także będzie błędny:
> (p_stdin, p_stdout, p_stderr, p_handle) <- runInteractiveCommand jakas_komenda
> hPut p_stdin jakies_dane
> odpowiedz <- hGet p_stdout
> odpowiedz_stderr <- hGet p_stderr
Dlaczego? Proces może zapełnić bufor p_stderr i zostać uśpiony zanim zamknie swoje standardowe wyjście (dzięki czemu wywołanie "hGet p_stdout" się skończyłoby się i zaczelibyśmy opróżniać p_stderr).
Rozwiązanie w tym przypadku jest nieco bardziej skomplikowane, jednak zaczyna się tutaj pojawiać pewien schemat:
> (p_stdin, p_stdout, p_stderr, p_handle) <- runInteractiveCommand jakas_komenda
> forkIO (hPut p_stdin jakies_dane)
> mv <- newEmptyMVar :: IO (MVar String)
> forkIO (hGet p_stdout >>= putMVar mv)
> odpowiedz_stderr <- hGet p_stderr
> odpowiedz <- takeMVar mv
(Dla opisu MVar przeczytaj ten post)
Co się tutaj wydarzyło? To co poprzednio: dodaliśmy nowy wątek który zajmuje się obsługą wejścia/wyjścia dla dokładnie jednego uchwytu (Handle).
Zauważmy, że w poprawnym kodzie mamy dokładnie jeden wątek dla jednego uchwytu: jeden "główny" oraz dwa utworzone przez forkIO. Jest to ogólna reguła by unikać tego typu zakleszczeń.
Uważny czytelnik zauważy, że w pewnym momencie odszedłem od słowa "kanał" (pipe) na korzyść słowa "uchwyt" (handle). O ile te pierwsze występują w przypadku komunikacji między procesami - i ten przypadek rozważamy - o tyle ten typ błędu występuje ogólnie dla typu uchwytów, które w GHC wykorzystywane są dla wielu typów operacji wejścia wyjścia - w szczególności dla połączeń sieciowych. W ich przypadku również może dochodzić do tego typu błędów.
W każdym z powyżej zacytowanych kawałków kodu jest czai się jeszcze jeden typ błędu, wynikający z semantyki uchwytów w GHC. Jak możemy przeczytać w dokumentacji nieużywany uchwyt jest automatycznie zamykany przez odśmiecacz (GC - garbage collector). Ma to ważną implikację: nie mamy gwarancji kiedy to nastąpi. Dlatego też może się zdarzyć, że otworzymy zbyt wiele plików na raz i system odmówi nam otwarcia nowych deskryptorów pliku. RTS wyrzuci nam w tym momencie wyjątek którego prawdopodobnie nie złapiemy - i nasz program zostanie zabity. Stąd ważny nawyk programistyczny: nieużywane uchwyty zamykamy tak szybko jak tylko przestają nam być potrzebne i nie liczymy w tym przypadku na pomoc systemu.
Dla kompletności oto poprawny (mam nadzieję...) kod:
> (p_stdin, p_stdout, p_stderr, p_handle) <- runInteractiveCommand jakas_komenda
> forkIO (hPut p_stdin jakies_dane >> hClose p_stdin)
> mv <- newEmptyMVar :: IO (MVar String)
> forkIO (hGet p_stdout >>= putMVar mv >> hClose p_stdout)
> odpowiedz_stderr <- hGet p_stderr
> odpowiedz <- takeMVar mv
Zadanie jest proste: uruchamiamy pewien proces, karmimy go danymi i oczekujemy od niego odpowiedzi. Naiwny kod wygląda tak:
> (p_stdin, p_stdout, p_stderr, p_handle) <- runInteractiveCommand jakas_komenda
> hPut p_stdin jakies_dane
> odpowiedz <- hGet p_stdout
Powyższy kod może, ale nie musi zadziałać. Co więcej: może raz działać dla danego programu, a raz nie działać. Prowadzi to do trudnych do debugowania i przez to irytujących błędów. Niestety: problem wynika z nieznajomości semantyki kanałów (ang. pipe - moje tłumaczenie jest chałupnicze) pomiędzy procesami.
Połączenia te mają ograniczoną pojemność "bufora". Po jego zapełnieniu proces który próbuje go "przepełnić" jest usypiany, aż bufor zostanie nieco opróżniony. Jest to (całkowicie słuszny) środek zaradczy przeciwko zużyciu przez nadgorliwy proces wszelkich zasobów systemu - w przeciwnym wypadku system musiałby przechować dowolnie dużo danych które zapisał dany proces. Bufor ten może być dość niewielki, np. 300 kb.
Jaki to ma związek z powyższym kodem? Ano taki, że w wyniku takiego właśnie działania kanałów kod ten powoduje często deadlocks - zakleszczenia.
Oto co się dzieje.
Uruchamiamy wysyłanie danych do naszego procesu:
> hPut p_stdin jakies_dane
Ale ów proces nie konsumuje ich na raz w całości. Zamiast tego zaczyna wysyłać częściowe porcje danych które wędrują do p_stdout. Wysyła ich na tyle dużo, że bufor p_stdout zapełnia się. Zostaje więc uśpiony. Aby został obudzony musimy odebrać z p_stdout porcję danych. Ale nie możemy tego zrobić - jeszcze nie skończyliśmy wysyłać mu danych na p_stdin!
Rozwiązanie jest proste: należy uruchomić wysyłanie danych w innym wątku:
> forkIO (hPut p_stdin jakies_dane)
W tym momencie jeden wątek będzie realizował wysyłanie danych, a drugi odbieranie. Co prawda jeden z nich może zostać uśpiony (bo np. proces nie odebrał jeszcze wszystkich danych i wykonuje teraz jakieś obliczenia) ale nie spowoduje to zakleszczenia.
Scenariusz może się jeszcze bardziej skomplikować, jeżeli interesuje nas równocześnie wyjście z p_stderr. W tym momencie ten kod także będzie błędny:
> (p_stdin, p_stdout, p_stderr, p_handle) <- runInteractiveCommand jakas_komenda
> hPut p_stdin jakies_dane
> odpowiedz <- hGet p_stdout
> odpowiedz_stderr <- hGet p_stderr
Dlaczego? Proces może zapełnić bufor p_stderr i zostać uśpiony zanim zamknie swoje standardowe wyjście (dzięki czemu wywołanie "hGet p_stdout" się skończyłoby się i zaczelibyśmy opróżniać p_stderr).
Rozwiązanie w tym przypadku jest nieco bardziej skomplikowane, jednak zaczyna się tutaj pojawiać pewien schemat:
> (p_stdin, p_stdout, p_stderr, p_handle) <- runInteractiveCommand jakas_komenda
> forkIO (hPut p_stdin jakies_dane)
> mv <- newEmptyMVar :: IO (MVar String)
> forkIO (hGet p_stdout >>= putMVar mv)
> odpowiedz_stderr <- hGet p_stderr
> odpowiedz <- takeMVar mv
(Dla opisu MVar przeczytaj ten post)
Co się tutaj wydarzyło? To co poprzednio: dodaliśmy nowy wątek który zajmuje się obsługą wejścia/wyjścia dla dokładnie jednego uchwytu (Handle).
Zauważmy, że w poprawnym kodzie mamy dokładnie jeden wątek dla jednego uchwytu: jeden "główny" oraz dwa utworzone przez forkIO. Jest to ogólna reguła by unikać tego typu zakleszczeń.
Uważny czytelnik zauważy, że w pewnym momencie odszedłem od słowa "kanał" (pipe) na korzyść słowa "uchwyt" (handle). O ile te pierwsze występują w przypadku komunikacji między procesami - i ten przypadek rozważamy - o tyle ten typ błędu występuje ogólnie dla typu uchwytów, które w GHC wykorzystywane są dla wielu typów operacji wejścia wyjścia - w szczególności dla połączeń sieciowych. W ich przypadku również może dochodzić do tego typu błędów.
W każdym z powyżej zacytowanych kawałków kodu jest czai się jeszcze jeden typ błędu, wynikający z semantyki uchwytów w GHC. Jak możemy przeczytać w dokumentacji nieużywany uchwyt jest automatycznie zamykany przez odśmiecacz (GC - garbage collector). Ma to ważną implikację: nie mamy gwarancji kiedy to nastąpi. Dlatego też może się zdarzyć, że otworzymy zbyt wiele plików na raz i system odmówi nam otwarcia nowych deskryptorów pliku. RTS wyrzuci nam w tym momencie wyjątek którego prawdopodobnie nie złapiemy - i nasz program zostanie zabity. Stąd ważny nawyk programistyczny: nieużywane uchwyty zamykamy tak szybko jak tylko przestają nam być potrzebne i nie liczymy w tym przypadku na pomoc systemu.
Dla kompletności oto poprawny (mam nadzieję...) kod:
> (p_stdin, p_stdout, p_stderr, p_handle) <- runInteractiveCommand jakas_komenda
> forkIO (hPut p_stdin jakies_dane >> hClose p_stdin)
> mv <- newEmptyMVar :: IO (MVar String)
> forkIO (hGet p_stdout >>= putMVar mv >> hClose p_stdout)
> odpowiedz_stderr <- hGet p_stderr
> odpowiedz <- takeMVar mv
niedziela, listopada 09, 2008
I na co tu się zdecydować?
Bardzo wielu programistów robi od czasu do czasu projekty "dla siebie". Nie ma w tym nic dziwnego: jest to okazja na stworzenie czegoś ciekawego i mniej lub bardziej użytecznego. Dla mnie najgorszym ograniczeniem w realizacji tego typu projektów jest czas: nie ma go na tyle by móc rozwijać wszystkie pomysły które przyjdą mi do głowy. Aktualnie mam ich kilka:
I za co mam się zabrać?
- MusicFS: Wirtualny system plików pod FUSE, odzwierciedlający kolekcję plików muzycznych. Podobne do biblioteki multimediów z foobar2000 czy WinAmpa. Za: potencjalnie przydatny, wykorzystanie FUSE, wykorzystanie baz danych (akurat na projekt z baz). Przeciw: nie potrzebuje tego tak bardzo, bo bibliotekę mam w foobar2000.
- SnapshotFS: podobnie jak wyżej wirtualny system plików pod FUSE. Prowadzi w tle kontrolę wersji, commity wykonywane w tle przy odmontowywaniu systemu, w wybranych odstępach czasu i na rządanie wysyłane przez dbus
- WikiTranslate: program tłumaczący słowa z użyciem Wikipedii. Wyszukujemy hasło spośród wpisów na zadanej wersji językowej Wikipedii a nastepnie patrzymy na linki odsyłające do wersji artykułu napisanej w innych językach. Za: teraz muszę robić ten proces ręcznie. Przeciw: dość skomplikowana heurystyka, nieprzyjemne parsowanie strony
- WikiTranslate-HAppS: interfejs webowy do WikiTranslate + cache. Za: możliwość udostępnienia narzędzia szerszej publiczności. Przeciw: trzeba najpierw napisać WikiTranslate, potrzebny jest serwer na którym można by to postawić
- UniNotifier: demon obserwujący wybrane strony www i feedy. Przy znaczącej modyfikacji wysyła informację. Za: bardzo przydatne. Przeciw: dla każdej strony trzebaby pisać z natury brzydki ekstraktor istotnych informacji
I za co mam się zabrać?
sobota, listopada 08, 2008
gitit - silnik wiki napisany w Haskellu
Dzisiaj na listę mailingową Haskell-Cafe trafiła wiadomość ogłaszająca wydanie wersji 0.2 programu o nazwie gitit. Jest to silnik wiki oparty o serwer HAppS, system kontroli wersji git, i bibliotekę do konwersji formatów znacznikowych pandoc.
Instalacja gitit z wykorzystaniem programu cabal-install jest bardzo prosta:
cabal update
cabal install pandoc -fhighlighting
cabal install gitit
A potem wystarczy już uruchomić go komendą
gitit
i połączyć się na http://localhost:5001
W trakcie instalacji ściągną się i skompilują automatycznie wszystkie potrzebne moduły. Niedawno gdy próbowałem zainstalować w podobny sposób samo HAppS okazało się, że pakiet ten wymaga pakietu unix, który z oczywistych względów nie zbuduje się zbyt łatwo pod Windowsem. Próbowałem tego dokonać z wykorzystaniem Cygwina - bezskutecznie.
Niestety także i teraz rozbiłem się o ten problem. Nie chciało mi się za bardzo przełączać na Linuxa tylko po to by zainstalować ten program. Na dobrą sprawę mogę przecież obejrzeć jego demo tutaj: http://johnmacfarlane.net:5001/.
Moje wrażenia? Jak na tak wczesną wersję zapowiada się całkiem ciekawie. Sprytnym posunięciem jest skorzystanie z gita do zarządzania wersjami: w końcu nie ma sensu wymyślać dwa razy koła, prawda? Chętnie zobaczę jak działać będą przyszłe wersje tej aplikacji.
Instalacja gitit z wykorzystaniem programu cabal-install jest bardzo prosta:
cabal update
cabal install pandoc -fhighlighting
cabal install gitit
A potem wystarczy już uruchomić go komendą
gitit
i połączyć się na http://localhost:5001
W trakcie instalacji ściągną się i skompilują automatycznie wszystkie potrzebne moduły. Niedawno gdy próbowałem zainstalować w podobny sposób samo HAppS okazało się, że pakiet ten wymaga pakietu unix, który z oczywistych względów nie zbuduje się zbyt łatwo pod Windowsem. Próbowałem tego dokonać z wykorzystaniem Cygwina - bezskutecznie.
Niestety także i teraz rozbiłem się o ten problem. Nie chciało mi się za bardzo przełączać na Linuxa tylko po to by zainstalować ten program. Na dobrą sprawę mogę przecież obejrzeć jego demo tutaj: http://johnmacfarlane.net:5001/.
Moje wrażenia? Jak na tak wczesną wersję zapowiada się całkiem ciekawie. Sprytnym posunięciem jest skorzystanie z gita do zarządzania wersjami: w końcu nie ma sensu wymyślać dwa razy koła, prawda? Chętnie zobaczę jak działać będą przyszłe wersje tej aplikacji.
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.
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
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.
- Nazwanie modułu
- Importowanie odpowiednich bibliotek.
- Inicjalizacja GLFW
- Funkcje pomocnicze
- Dalsze ustawianie środowiska
- Główna pętla renderingu
- A jak stąd wyjść?
- Sprzątamy po sobie...
Warto jest nazwać jakoś nasz program. Umieszczamy więc w pierwszej linii:
>>> module Main where
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
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.
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.
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).
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.
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
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
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
Subskrybuj:
Posty (Atom)