MSM napisał(a)
sam ten fakt powoduje mój podziw że z taką dużą aplikacją sobie poradzili bez efektów ubocznych, no ale widać to sprytni ludzie pisali
Ależ efekty uboczne są, zostały tylko ujęte w monady. Praktycznie dowolny stan da się wyrazić w formie czysto funkcyjnej, kolejne operacje w monadzie tworzą kolejne stany.
winerfresh napisał(a)
Oprócz OCamla masz jeszcze ML
OCaml to jest właśnie ML, jeden z jego dialektów.
winerfresh napisał(a)
Gedit + haskell
Gedit? Niee no, bez przesady, przecież to zwykły notatnik, nie oferuje nawet integracji z GHCi. Poza tym nie "+ haskell" tylko "+ GHC", jest kilka środowisk tego języka, tylko jedno naprawdę warte uwagi. Z edytorów najpopularniejszy jest Emacs, http://www.haskell.org/haskellwiki/Haskell_mode_for_Emacs - opisane co i jak.
Co do Twojego kodu w Haskellu to można mieć pewne zastrzeżenia - nie stosujesz pattern matchingu, nadużywasz pattern guards, efekty uboczne (i to w IO
) stosujesz jakby to był jakiś język imperatywny, kod przechodzi dzięki uprzejmości GHC bo main
ma niewłaściwy typ.
winerfresh napisał(a)
Kopiuj
sklej n | n == 1 = 1
| n `mod` 2 == 0 = n - 1 + 2 * sklej (n `div` 2)
| n `mod` 2 /= 0 = n - 1 + sklej ((n-1) `div` 2) + sklej((n+1) `div` 2)
Pattern guard nie stosuje się tam, gdzie sam pattern matching wystarcza, przypadek bazowy:
Nie tworzy się przeciwnego guarda, tak jak dla if
używa się else
, tak tutaj ostatnia klauzula to zwyczajowo otherwise
(co jest definiowane po prostu jako otherwise = True
). Od czegoś są też wbudowane predykaty. Przypadek ogólny:
Kopiuj
sklej' n | even n = n - 1 + 2 * sklej' (n `div` 2)
| otherwise = n - 1 + sklej' ((n-1) `div` 2) + sklej' ((n+1) `div` 2)
Dalej można np. wyodrębnić operację sklej $ div (f n) 2
ale to już raczej zwykła zabawa.
winerfresh napisał(a)
Kopiuj
main = mapM (print . sklej) [1..10000]
Funkcja main
powinna mieć typ IO ()
, u Ciebie ma IO [()]
, zastosowałeś niewłaściwą funkcję mapującą, monadyczny map
wołany wyłącznie dla efektów ubocznych to mapM_
, nie mapM
. Dla GHC nie ma to wielkiego znaczenia, wynik main
jest nieużywany. Po każdej operacji wywołujesz efekty uboczne dla "świata", operacje "czyste" powinno się od nich oddzielać, jak największa część programu powinna być niezależna. W tym wypadku można efektywnie posłużyć się pojedynczą operacją IO, pojedynczym putStrLn
(print = putStrLn . show
), dzięki leniwej ewaluacji i buforowaniu możliwe jest zbudowanie całego wyjścia w miarę potrzeb, "czysto":
Kopiuj
main0 = putStrLn $ unlines $ map (show . sklej') [0..10000]
Dzięki temu, że pojedyncza operacja IO konsumuje wyjście wedle potrzeb mamy zauważalny przyrost wydajności, u mnie około 15% (na ideone zysk wychodzi zdecydowanie mniejszy). Ważne: wyniki zaczynają pojawiać się od razu, nie po zakończeniu obliczeń, tak działa leniwa ewaluacja.
Zeby nie było nudno, możemy skorzystać z faktu, że lista jest monadą (a String
to [Char]
), np. w ten sposób:
Kopiuj
main1 = putStrLn $ do
n <- [1..10000]
show (sklej' n) ++ "\n"
Wygląda nawet "imperatywnie", nie? :] Przyda Ci się trochę praktyki, pisanie w Haskellu to nie tylko zamiana pętli na rekurencję.
Rozwiązanie w C zawiera algorytm liniwy w czasie i przestrzeni, wypełnienie tablicy n-elementowej. Hmmm, tablice w kodzie funkcyjnym nie są zbyt często spotykane, ale gdyby tak zrobić nieskończoną listę? Zrobić jednolinijkowca, który mieści się na standardowym ekranie (80 kolumn) i korzysta z dwóch monad chociaż jest "czysty"? No way?
Kopiuj
fill = 1 : (zipWith (+) [1..] (tail >>= zipWith (+) $ replicate 2 =<< fill))
Piękny i czytelny kod, nie trzeba znać zachowania monady (->) a
aby domyślić się działania. Złożoność pamięciowa to w najgorszym wypadku O(n), gdzie w C była ona gwarantowana. Lista budowana jest leniwie, tylko tyle, ile aktualnie trzeba, druga połowa listy jest potrzebna do budowy kolejnych elementów, pierwsza może zostać szybko zutylizowana przez GC. Nie możemy efektywnie indeksować listy, możemy jednak zbudować rekurencję wokół niej, wedle potrzeb podwajając elementy zamiast stosować indeks i / 2
. Algorytm tablicowy używał pary sąsiednich elementów, tak też i my robimy, sumujemy głowy aktualnej części "podwojonej" listy i jej ogona. Rozpisując:
Kopiuj
tail >>= zipWith (+) $ xs
otrzymujemy:
Kopiuj
zipWith (+) (tail xs) xs
Ważne dla nas jest złączenie operacji i powielenie pojedynczego argumentu dla dwóch połączonych funkcji. "Trzecia" lista ([1..]
) fizycznie nie istnieje, generowane elementy są używane i zapominane, nie są nigdzie zapisywane. No dobrze, to teraz jakoś trzeba to jeszcze odpalić i wypisać:
Kopiuj
main2 = putStrLn $ concat [ show x ++ "\n" | x <- take 10000 fill ]
Tym razem z użyciem list comprehension, połączenie concat
i (++ "\n")
jest równoważne unlines
. Ostatnie mainy są "rozwlekłe" ponieważ są nastawione na prezentowanie różnych konstrukcji w języku.
Podobnych efektów nie da się uzyskać bezpośrednio np. w OCamlu. Scala ma streamy, niestety stream można przejść tylko jednokrotnie. To już zadanie dla Wibowita, za kiepsko Scalą władam żeby jakieś obejście na szybko znaleźć. Co do Haskella to ktoś bardziej doświadczony powinien się wypowiedzieć, dla mnie to narzędzie nauki i intelektualna zabawka.
Kody na ideone:
link |
opis |
czas |
pamięć |
http://ideone.com/rgT1Q |
oryginalny kod |
0.45s |
3596 kB |
http://ideone.com/CBuKQ |
z poprawkami |
0.42s |
3596 kB |
http://ideone.com/9jUmx |
nieskończona lista |
0.01s |
4616 kB |
Zużycie pamięci trochę wzrosło, ale to GC, prealokuje większe bloki, w dwóch pierwszych kodach program mógł się obejść całkowicie bez alokacji. Z drugiej strony czas wykonywania jakby się skrócił :] |
|
|
|
Trochę rozrywki intelektualnej z rana, Kumashiro zdominował jednolinijkowce w Pythonie, mnie też się coś od życia należy, miłego dnia.