Scheme dla każdego

Synthroid Without Prescription Inderal No Prescription Nexium For Sale Prevacid Generic Buy Elimite Online Prevacid Without Prescription Ultram No Prescription Prevacid For Sale Ultram Generic Buy Prednisone Online

W tym tygodniu skończyłem czytać wprowadzenie do języka Haskell o zachęcającym tytule “Write Yourself a Scheme in 48 Hours”. W języku statycznie-typowanym, leniwym i bez śladu efektów ubocznych implementujemy język dynamiczny, ze ścisłą ewaluacją i mutacją. Ciekawe doświadczenie samo w sobie, chociaż możliwe, że zbyt wymagające dla monolingwistów. Autor nie bez przyczyny podaje Wizard Booka i Małego Schemera jako lektury uzupełniające. Bez tego rzeczywiście może być trudno.

Poradnik czyta się dość lekko, można wręcz popaść w rutynę kopiuj-wklej i przerobić całość w krótkim czasie. Warto jednak pomedytować nad samym tekstem tutoriala, a obowiązkowo już nad pojawiającymi się co jakiś czas ćwiczeniami. Dzięki nim można oswoić się z dokumentacją i poczuć czym pisanie programu w Haskellu naprawdę się je. Niestety pod koniec tekstu, właśnie wtedy, gdy pojawiają się trudniejsze tematy, jak łączenie monadów i zarządzanie stanem, ćwiczeń zaczyna brakować. Nie ma innej rady, jak zagłębić się w tekst i linijka po linijce prześledzić kod.

Ja tutaj ograniczę się do przedstawienia kilku subiektywnych spostrzerzeń na temat samego Haskella.

Rzucanie i łapanie wyjątków

Moją uwagę szczególnie zwróciły funkcje obsługi wyjątków przedstawione w części 5. Po raz pierwszy widząc definicję extractValue uznałem ją za błędną:
extractValue :: ThrowsError String -> String
extractValue (Right val) = val
extractValue ma typ ThrowsError String -> String, zdaje się więc uwalniać wartość z paszczy monadu. Co więcej, extractValue zdefiniowany jest tylko dla konstruktora Right, tutaj obudowującego prawidłową wartość obliczeń, podczas gdy Left obudowuje wartość typu LispError. Okazuje się, że definicja taka nie sprawia problemów, gdyż autor używa extractValue wyłącznie po wywołaniu trapError.
evalString :: String -> IO String
evalString expr = return $ extractValue $
    trapError (liftM show $ readExpr expr >>= eval)

trapError korzysta z catchError, by wszystkie ewentualne błędy w action przepuścić przez funkcję (return . show):

trapError :: ThrowsError String -> ThrowsError String
trapError action = catchError action (return . show)

to zaś jedynie zamienia błąd na ciąg znaków (show) i obudowuje go jako wartość (return). trapError nigdy więc nie zwróci wartości Left i extractValue jest bezpieczne w swojej obecnej definicji.

Przykład ten jest ciekawy, gdyż pokazuje, że można pisać funkcje, które korzystają z monadu tylko wewnętrzenie, nie “zarażając” pozostałych części systemu. Niestety nie zawsze możliwa jest bezpieczna ucieczka z monadu. catchError wyraźnie oddziela część programu, która może zgłaszać błędy od reszty i poprzez funkcję obsługi błędu (drugi argument do catchError) pozwala obliczenia błędne zamienić na prawidłowe, tak jak w przykładzie powyżej robi to złożenie return i show.

Gdy jednak spojrzymy na monad IO, sytuacja nie jest już taka różowa. Co prawda istnieje funkcja unsafePerformIO, ale nie jest ona, jak z resztą jej nazwa wskazuje, bezpieczna w użyciu. O ile bowiem funkcję zgłaszającą błąd łatwo przekształcić na taką, która błędu nie zgłasza (kto nie słyszał o łapaniu wyjątków?), to by zamienić funkcję, która skorzystała z wejścia/wyjścia na taką, która z niego nie skorzystała, potrzebowalibyśmy wehikułu czasu.

Dla ciekawych tematu proponuję sprawdzić listę funkcji wypakowujących wartości z monadów: m a -> a.

Wyjątki mają odzwierciedlenie w typach

W stylu Javowych checked exceptions.

Mogą stanowić potencjalny PITA przy rozwijaniu programu. Gdy postanowimy wprowadzić obsługę błędów w pewnej części aplikacji, czeka nas uzupełnienie definicji o typ wyjątku, a samych funkcji o wywołania return. Nie jest to skomplikowana operacja, ale niestety żmudna i czasochłonna.

kapsW

Płynne posługiwanie się Haskellem wymaga opanowania czytania wyrażeń od końca. Składanie funkcji, styl pointfree łącznie z biblioteką standardową pełną procedur wyższego rzędu powoduje, że definicje takie jak isBound są typowe dla programów w Haskellu:

isBound envRef var = readIORef envRef >>= return .
    maybe False (const True) . lookup var

Słowami samego autora:

This style of programming - relying heavily on function composition, function application, and passing functions to functions - is very common in Haskell code. It often lets you express very complicated algorithms in a single line, breaking down intermediate steps into other functions that can be combined in various ways. Unfortunately, it means that you often have to read Haskell code from right-to-left and keep careful track of the types.

Podstawowy przykład użycia IORef

Jeżeli przedstawiony w części 8 opis IORef nie był zbyt jasny, znalazłem przejrzysty przykład użycia IORef.