Dynamiczna wysyłka w Haskell

Programy napisane na przykład w Javie w dużym stopniu polegają na dynamicznym wysyłaniu. Jak takie programy są wyrażane w językach funkcyjnych, takich jak Haskell? Innymi słowy, jaki jest Haskell sposób wyrażenia idei pod "dynamiczna wysyłka"?

Author: Duncan Jones, 2012-10-28

4 answers

Odpowiedź jest zwodniczo prosta: funkcje wyższego rzędu. Obiekt z wirtualnymi metodami w języku OO jest niczym innym jak tylko zapisem funkcji wraz z pewnym stanem lokalnym. W Haskell możesz używać zapisów funkcji bezpośrednio i przechowywać stan lokalny w ich zamknięciach.

Konkretniej, obiekt OO składa się z:

  • wskaźnik (vptr) do vtable (virtual method table), który zawiera implementacje dla wirtualnych metod obiektu klasy. Innymi słowy, zbiór wskaźników funkcji; zapis funkcji. W szczególności każda funkcja ma ukryty parametr, który jest samym obiektem, który jest przekazywany niejawnie.
  • członkowie danych obiektu (stan lokalny)

Przez większość czasu cały Gmach obiektów i funkcji wirtualnych wydaje się misternym obejściem braku wsparcia dla zamknięć.

Na przykład rozważ interfejs Javy Comparator:

public interface Comparator<T> {
    int compare(T o1, T o2); // virtual (per default)
}

I przypuśćmy, że chcesz aby użyć go do sortowania listy łańcuchów na podstawie N-tych znaków łańcuchów (Załóżmy, że są wystarczająco długie). Można zdefiniować klasę:

public class MyComparator implements Comparator<String> {
    private final int _n;

    MyComparator(int n) {
        _n = n;
    }

    int compare(String s1, String s2) {
        return s1.charAt(_n) - s2.charAt(_n);
    }
}

A potem go używasz:

Collections.sort(myList, new MyComparator(5));      

W Haskell zrobiłbyś to tak:

sortBy :: (a -> a -> Ordering) -> [a] -> [a]

myComparator :: Int -> (String -> String -> Ordering)
myComparator n = \s1 s2 -> (s1 !! n) `compare` (s2 !! n)
-- n is implicitly stored in the closure of the function we return

foo = sortBy (myComparator 5) myList
Jeśli nie jesteś zaznajomiony z Haskellem, oto jak z grubsza wyglądałoby to w rodzaju pseudo-Javy: (zrobię to tylko raz)]}
public void <T> sortBy(List<T> list, Ordering FUNCTION(T, T) comparator) { ... }

public (Ordering FUNCTION(String, String)) myComparator(int n) {
    return FUNCTION(String s1, String s2) {
        return s1[n].compare(s2[n]);
    }
}

public void foo() {
    sortBy(myList, myComparator(5));
}

Zauważ, że nie zdefiniowaliśmy żadnych typów. Użyliśmy tylko funkcji. W obu przypadkach "ładunek", który przekazaliśmy do funkcji sortowania funkcję, która bierze dwa elementy i daje ich względny porządek. W jednym przypadku zostało to osiągnięte przez zdefiniowanie typu implementującego interfejs, zaimplementowanie jego wirtualnej funkcji w odpowiedni sposób i przekazanie obiektu tego typu; w drugim przypadku po prostu przekazaliśmy funkcję bezpośrednio. W obu przypadkach przechowywaliśmy wewnętrzną liczbę całkowitą w rzeczy, którą przekazaliśmy do funkcji sortowania. W jednym przypadku zostało to zrobione przez dodanie prywatnego członka danych do naszego typu, w drugi, po prostu odwołując się do niego w naszej funkcji, powodując, że zostanie zachowany w zamknięciu funkcji.

Rozważ bardziej rozbudowany przykład widżetu z procesorami obsługi zdarzeń:

public class Widget {
    public void onMouseClick(int x, int y) { }
    public void onKeyPress(Key key) { }
    public void paint() { }
    ...
}

public class MyWidget extends Widget {
    private Foo _foo;
    private Bar _bar;
    MyWidget(...) {
        _foo = something;
        _bar = something; 
    }
    public void onMouseClick(int x, int y) {
        ...do stuff with _foo and _bar...
    }
}

W Haskell możesz to zrobić tak:

data Widget = Widget {
    onMouseClick :: Int -> Int -> IO (),
    onKeyPress   :: Key -> IO (),
    paint        :: IO (),
    ...
}

constructMyWidget :: ... -> IO Widget
constructMyWidget = do
    foo <- newIORef someFoo
    bar <- newIORef someBar
    return $ Widget {
        onMouseClick = \x y -> do
            ... do stuff with foo and bar ...,
        onKeyPress = \key -> do ...,
        paint = do ...
    }

Zauważ jeszcze raz, że po początkowym Widget nie zdefiniowaliśmy żadnych typów. Napisaliśmy tylko funkcję, aby zbudować zapis funkcji i przechowywać rzeczy w ich zamknięciach. Co w większości przypadków jest również jedynym powodem do zdefiniowania podklasa w języku OO. Jedyną różnicą od naszego poprzedniego przykładu jest to, że zamiast jednej funkcji istnieje wiele, które w przypadku Javy jest kodowane przez zwykłe umieszczenie więcej niż jednej funkcji w interfejsie (i jego implementacji), a w Haskell przez przekazanie rekordu funkcji zamiast pojedynczej funkcji. (Mogliśmy przekazać rekord zawierający jedną funkcję w poprzednim przykładzie, ale nie mieliśmy na to ochoty.)

(warto gdzieś zauważyć, że wiele czas nie potrzebujesz dynamicznej wysyłki. Jeśli chcesz posortować listę na podstawie domyślnej kolejności dla danego typu, po prostu użyj sort :: Ord a => [a] -> [a], która używa instancji Ord zdefiniowanej dla danego typu a, która jest wybierana statycznie.)

Dynamiczna Wysyłka Typu

Jedną z różnic pomiędzy podejściem Java a podejściem Haskella jest to, że w podejściu Java zachowanie obiektu (poza jego stanem lokalnym) zależy od jego typu (lub mniej przychylnie, każda implementacja wymaga nowego typu). W Haskell robimy nasze zapisy funkcji, jak tylko chcemy. W większości przypadków jest to czysta Wygrana( elastyczność zyskana, nic straconego), ale załóżmy, że z jakiegoś powodu chcemy tego w sposób Java. W takim przypadku drogą, jak wspominają inne odpowiedzi, są klasy typu i existentials.

Aby kontynuować nasz przykład Widget, Załóżmy, że chcemy, aby implementacja Widget następowała od jego typu (wymagała nowego typ dla każdej implementacji). Definiujemy klasę type, aby odwzorować typ na jego implementację:

-- the same record as before, we just gave it a different name
data WidgetImpl = WidgetImpl {
    onMouseClick :: Int -> Int -> IO (),
    onKeyPress   :: Key -> IO (),
    paint        :: IO (),
    ...
}

class IsWidget a where
    widgetImpl :: a -> WidgetImpl

data Widget = forall a. IsWidget a => Widget a

sendClick :: Int -> Int -> Widget -> IO ()
sendClick x y (Widget a) = onMouseClick (widgetImpl a) x y

data MyWidget = MyWidget {
    foo :: IORef Foo,
    bar :: IORef Bar
}

constructMyWidget :: ... -> IO MyWidget
constructMyWidget = do
    foo_ <- newIORef someFoo
    bar_ <- newIORef someBar
    return $ MyWidget {
        foo = foo_,
        bar = bar_
    }

instance IsWidget MyWidget where
    widgetImpl myWidget = WidgetImpl {
            onMouseClick = \x y -> do
                ... do stuff with (foo myWidget) and (bar myWidget) ...,
            onKeyPress = \key -> do ...,
            paint = do ...
        }

To trochę niezręczne, że mamy klasę tylko po to, aby uzyskać zapis funkcji, z których następnie musimy wyjąć funkcje osobno. Zrobiłem to tylko w ten sposób, aby zilustrować oddzielne aspekty klas typów: są to również po prostu gloryfikowane rekordy funkcji (których używamy poniżej) wraz z pewną magią, w której kompilator wstawia odpowiedni rekord oparty na wywnioskowanym typie (który używamy powyżej i nadal używamy poniżej). Uprośćmy:

class IsWidget a where
    onMouseClick :: Int -> Int -> a -> IO ()
    onKeyPress   :: Key -> a -> IO ()
    paint        :: a -> IO ()
    ...

instance IsWidget MyWidget where
    onMouseClick x y myWidget = ... do stuff with (foo myWidget) and (bar myWidget) ...
    onKeyPress key myWidget = ...
    paint myWidget = ...

sendClick :: Int -> Int -> Widget -> IO ()
sendClick x y (Widget a) = onMouseClick x y a

-- the rest is unchanged from above

Ten styl jest często przyjmowany przez osoby wywodzące się z języków OO, ponieważ jest bardziej znany i bliższy odwzorowaniu jeden na jednego ze sposobu, w jaki języki OO to robią. Ale dla większości celów jest to po prostu bardziej rozbudowane i mniej elastyczne niż podejście opisane w pierwszej sekcji. Powodem jest to, że jeśli jedyną istotną rzeczą w różnych widżetów jest sposób, w jaki implementują funkcje widżetu, to niewiele wskaż w tworzenie typów, instancji interfejsu dla tych typów, a następnie ponownie abstrakcję bazowego typu, umieszczając je w egzystencjalnym opakowaniu: łatwiej jest po prostu przekazać funkcje bezpośrednio.

Jedną z zalet, o których mogę myśleć, jest to, że chociaż Haskell nie ma podtypu, ma "podklasowanie" (prawdopodobnie lepiej określane jako sub-interfacing lub sub-ograniczenie). Na przykład można zrobić:

class IsWidget a => IsWidgetExtra a where
    ...additional methods to implement...

A potem z dowolnym typem, dla którego mieć IsWidgetExtra, można również bezproblemowo korzystać z metod IsWidget. Jedyną alternatywą dla podejścia opartego na zapisach jest posiadanie rekordów w rekordach, co wiąże się z ręcznym owijaniem i rozpakowywaniem wewnętrznych rekordów. Ale byłoby to korzystne tylko, gdybyś chciał wyraźnie naśladować głęboką hierarchię klas języka OO, co z kolei zrobiłbyś tylko, gdybyś chciał utrudnić sobie życie. (Zauważ również, że Haskell nie ma żadnego wbudowanego sposobu dynamicznego w dół-Obsada od IsWidget do IsWidgetExtra. Ale jest ifcxt )

(co z zaletami podejścia opartego na zapisach? Poza tym, że nie musisz definiować nowego typu za każdym razem, gdy chcesz zrobić nową rzecz, rekordy są prostymi rzeczami na poziomie wartości, a wartości są znacznie łatwiejsze do manipulowania niż typy. Można na przykład napisać funkcję, która przyjmuje Widget jako argument i na jej podstawie tworzy nową Widget, przy czym niektóre rzeczy są różne, a inne pozostają takie same. To jest coś w rodzaju podklasowanie z parametru szablonu w C++, tylko mniej mylące.)

Słowniczek

  • Funkcja wyższego rzędu: funkcja, która przyjmuje inne funkcje jako argumenty (lub zwraca je jako wyniki)

  • Record: struct(class with public data members and nothing else). Znany również jako słownik.

  • Języki funkcyjne (i wiele innych) pozwalają definiować lokalne funkcje (funkcje w funkcjach, lambdy), które sprawiają, że odniesienie do rzeczy znajdujących się w zakresie w miejscu definicji (na przykład argumenty funkcji zewnętrznej), których normalnie można oczekiwać, że nie będą przechowywane w pobliżu, ale są w "zamknięciu" funkcji. Alternatywnie, jeśli masz funkcję podobną do plus, która pobiera dwa int i zwraca int, możesz zastosować ją tylko do jednego argumentu, powiedzmy 5, a wynikiem będzie funkcja, która pobiera int i zwraca int, dodając do niej 5 - w tym przypadku 5 jest również przechowywana w wynikowej funkcji zamknięcie. (W innych kontekstach "closure" jest również czasami używane do określenia "funkcji z zamknięciem".)

  • Klasa typu: Nie taka sama jak klasa w języku OO. Coś w rodzaju interfejsu, ale też zupełnie innego. zobacz tutaj .

EDIT 29-11-14: chociaż myślę, że jądro tej odpowiedzi jest nadal zasadniczo poprawne (HOFs w Haskell odpowiadają metodom wirtualnym w OOP), moje oceny wartości wzrosły trochę niuansów od czasu, gdy ją napisałem. W szczególnie, teraz myślę, że ani podejście Haskella, ani OOP nie jest ściśle "bardziej fundamentalne" niż inne. Zobacz ten komentarz .

 63
Author: glaebhoerl,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 11:54:46

To zaskakujące, jak często nie potrzebujesz dynamicznej wysyłki, tylko polimorfizmu.

Na przykład, jeśli zamierzasz napisać funkcję, która sortuje wszystkie dane na liście, chcesz, aby była polimorficzna. (Tj., nie chcesz ręcznie zaimplementować tę funkcję dla każdego typu. To byłoby złe.) Ale tak naprawdę nie potrzebujesz niczego dynamicznego ; wiesz w czasie kompilacji, co tak naprawdę znajduje się na liście lub listach, które chcesz posortować. Więc ty w tym przypadku nie trzeba w ogóle szukać typu run-time.

W Haskell, jeśli chcesz po prostu przenosić rzeczy i nie musisz wiedzieć ani dbać o to, jaki to jest typ, możesz użyć tak zwanego "polimorfizmu parametrycznego", który jest mniej więcej czymś w rodzaju Java generics lub C++ templates. Jeśli chcesz mieć możliwość zastosowania funkcji do danych (np. do sortowania danych, których potrzebujesz do porównania kolejności), możesz przekazać funkcję, aby to zrobić jako argument. Alternatywnie, Haskell ma coś to trochę jak interfejs Java i można powiedzieć "Ta funkcja sortowania akceptuje każdy rodzaj danych, które implementują ten interfejs".

Jak na razie nie ma dynamicznego wysyłania, tylko statyczne. Zauważ również, że ponieważ możesz przekazać funkcje jako argumenty, możesz wykonać "dispatch" ręcznie ręcznie.

Jeśli naprawdępotrzebujesz rzeczywistejdynamicznej wysyłki, możesz użyć "typów egzystencjalnych", lub możesz użyć biblioteki Data.Dynamic i podobnych sztuczek.

 10
Author: MathematicalOrchid,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-11-28 09:08:18
 6
Author: Cfr,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-10-28 07:57:21

Może potrzebujesz ADT plus pattern matching ?

data Animal = Dog {dogName :: String}
            | Cat {catName :: String}
            | Unicorn

say :: Animal -> String
say (Dog {dogName = name}) = "Woof Woof, my name is " ++ name
say (Cat {catName = name}) = "Meow meow, my name is " ++ name
say Unicorn = "Unicorns do not talk"
 5
Author: s9gf4ult,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-10-28 07:22:41