Skip to content

Instantly share code, notes, and snippets.

@kleczkowski
Created August 16, 2020 21:09
Show Gist options
  • Save kleczkowski/bc351f4c60bba31c7ba1d7656b859176 to your computer and use it in GitHub Desktop.
Save kleczkowski/bc351f4c60bba31c7ba1d7656b859176 to your computer and use it in GitHub Desktop.

Obiekty w Haskellu?

Napisałem ten krótki referat, trochę w formie żartu, w jaki sposób możemy uzyskać obiekty w Haskellu. Haskell jest znany z tego, że daje łatwo wyrazić pewne problemy przez konstrukcje językowe, które są dla niego unikalne, a później często stają się inspiracją dla innych języków. Teraz inspiracją dla Haskella są obiekty.

Ale najpierw dynamic dispatch

Rozważmy sobie klasę typów IsTelegraph pozwalającą na wysyłanie i odbieranie pewnych danych za pomocą wejścia/wyjścia.

class IsTelegraph a where
  tgSend :: a -> String -> IO ()
  tgReceive :: a -> (String -> IO ()) -> IO ()

Mam nadzieję, że opis tych funkcji jest jasny.

Zdefiniujmy sobie parę struktur danych i instancji IsTelegraph.

data ConsoleTg = ConsoleTg

instance IsTelegraph ConsoleTg where
  tgSend _ = putStrLn
  tgReceive _ k = getLine >>= k

data FileTg = FileTg FilePath

instance IsTelegraph FileTg where
  tgSend (FileTg fp) = appendFile fp
  tgReceive (FileTg fp) k = readFile fp >>= k

(Dla FileTg funkcja tgReceive nie jest idealna, ale you got the point). Oczywiście taka konstrukcja nic nie wnosi, ponieważ i tak musimy znać typ, żeby móc uruchomić tgSend albo tgReceive, więc jest to zwykły Haskellowy kod.

Sytuacja nabiera kolorytu, jeśli dorzucimy tzw. existential types.

{-# LANGUAGE ExistentialQuantification #-}

data Telegraph = forall a. IsTelegraph a => Telegraph a

send :: Telegraph -> String -> IO ()
send (Telegraph a) msg = tgSend a msg

receive :: Telegraph -> (String -> IO ()) -> IO ()
receive (Telegraph a) k = tgReceive a k

Napiszmy również tzw. sprytne konstruktory.

toConsole :: IO Telegraph
toConsole = pure (Telegraph ConsoleTg)

toFile :: FilePath -> IO Telegraph
toFile fp = pure (Telegraph (FileTg fp))

Mając taki repertuar funkcji, możemy w pełni emulować obiekty. Zauważ, że korzystając z send i receive nie musimy nic znać na temat a, ponieważ powiedzieliśmy, że istnieje pewien typ a, który ma funkcje z IsTelegraph, na których możemy działać. Ponadto, możemy wyeksportować IsTelegraph (..), Telegraph i akompaniujące mu funkcje, kompletnie ukrywając fakt, że korzystamy z typów ConsoleTg oraz FileTg.

main
  = do stdout <- toConsole
       file   <- toFile "some/path/to/file.txt"
       send    stdOut "Sending to console"
       send    file   "Sending to file"
       receive stdOut putStrLn
       receive file   putStrLn

Pytacie pewnie, jak zrealizować mutowalny stan? Otóż jest to dość łatwa sprawa. Możemy po prostu zdefiniować odpowiednik ConsoleTg, bądź FileTg, żeby zawierał w sobie pole typu IO (IORef t), gdzie t to wartość, którą chcemy mutować. Wtedy możemy osiągnąć pełną obiektową nirwanę.

A dziedziczenie?

Dziedziczenie można zdefiniować różnie. Możemy tworzyć struktury w następujący sposób:

import Data.Char

newtype UpperCaseTg a = UCT a

instance IsTelegraph a => IsTelegraph (UpperCaseTg a) where
  tgSend (UCT a) msg = tgSend a (map toUpper msg)
  tgReceive (UCT a) k = tgReceive a (k . map toUpper)

Skorzystanie z instancji IsTelegraph jest analogiczne do korzystania z metod za pomocą super w Javie. Również nic nie przeszkadza nam, by kompletnie nadpisać zachowanie, nie korzystając z super. Zauważmy, że ta konstrukcja jest bardzo elastyczna, zasadniczo przypomina wzorzec Dekorator.

Jak wobec tego kontrolować dziedziczenie pól? Możemy wyeksportować poszczególne ukrywane struktury do osobnych modułów i eksportować wyłącznie wybrane pola. Mamy wtedy wybór między public a private i możemy to w taki sposób, mimo rozwlekłości, kontrolować.

Jedyną wadą tego rozwiązania jest fakt, że nie da się (bądź jeszcze nikt nie wpadł na pomysł) wyrazić poziomu protected. (Chociaż, tak szczerze, czy jest ono koniecznie potrzebne?)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment