Jak często seq jest używany w kodzie produkcyjnym Haskell?

23

Mam pewne doświadczenie w pisaniu małych narzędzi w Haskell i uważam, że korzystanie z nich jest bardzo intuicyjne, szczególnie w przypadku pisania filtrów (używania interact), które przetwarzają standardowe wejście i przesyłają je do standardowego wyjścia.

Ostatnio próbowałem użyć jednego takiego filtru w pliku, który był około 10 razy większy niż zwykle i dostałem Stack space overflowbłąd.

Po przeczytaniu (np. Tutaj i tutaj ) zidentyfikowałem dwie wskazówki, aby zaoszczędzić miejsce na stosie (doświadczeni Haskellerzy, poprawcie mnie, jeśli napiszę coś, co jest nieprawidłowe):

  1. Unikaj rekurencyjnych wywołań funkcji, które nie są rekurencyjne (dotyczy to wszystkich języków funkcjonalnych, które obsługują optymalizację wywołania ogona).
  2. Wprowadź, seqaby wymusić wczesną ocenę podwyrażeń, aby wyrażenia nie rosły zbyt duże, zanim zostaną zmniejszone (jest to specyficzne dla Haskell lub przynajmniej języków używających leniwej oceny).

Po wprowadzeniu pięciu lub sześciu seqwywołań w moim kodzie moje narzędzie ponownie działa płynnie (także w przypadku większych danych). Uważam jednak, że oryginalny kod był nieco bardziej czytelny.

Ponieważ nie jestem doświadczonym programistą Haskell, chciałem zapytać, czy wprowadzenie seqw ten sposób jest powszechną praktyką i jak często można to zobaczyć seqw kodzie produkcyjnym Haskell. Czy są jakieś techniki, które pozwalają uniknąć seqzbyt częstego używania i nadal zajmują mało miejsca na stosie?

Giorgio
źródło
1
Optymalizacje, takie jak te, które opisałeś, prawie zawsze powodują, że kod jest nieco mniej elegancki.
Robert Harvey
@Robert Harvey: Czy istnieją jakieś alternatywne techniki, aby utrzymać niskie zużycie stosu? Mam na myśli, że wyobrażam sobie, że muszę przepisać moje funkcje inaczej, ale nie mam pojęcia, czy istnieją dobrze ugruntowane techniki. Moja pierwsza próba polegała na użyciu funkcji rekurencji ogona, co pomogło, ale nie pozwoliło mi całkowicie rozwiązać problemu.
Giorgio

Odpowiedzi:

17

Niestety zdarzają się przypadki, gdy trzeba użyć seq, aby uzyskać wydajny / dobrze działający program do dużych danych. W wielu przypadkach nie można tego zrobić w kodzie produkcyjnym. Więcej informacji można znaleźć w rozdziale Real World Haskell, Rozdział 25. Profilowanie i optymalizacja .

Istnieją jednak możliwości uniknięcia seqbezpośredniego używania . Może to uczynić kod czystszym i bardziej niezawodnym. Jakieś pomysły:

  1. Zamiast tego użyj kanału , rur lub iteratówinteract . Leniwe IO ma problemy z zarządzaniem zasobami (nie tylko pamięcią), a iteraty są zaprojektowane tak, aby to rozwiązać. (Sugeruję unikanie leniwego We / Wy niezależnie od tego, jak duże są twoje dane - zobacz Problem z leniwymi We / Wy .)
  2. Zamiast seqbezpośrednio używać (lub projektować własne) kombinatory, takie jak foldl ' lub foldr' lub ścisłe wersje bibliotek (takich jak Data.Map.Strict lub Control.Monad.State.Strict ), które są przeznaczone do ścisłych obliczeń.
  3. Użyj rozszerzenia BangPatterns . Pozwala zastąpić seqścisłym dopasowaniem wzorca. W niektórych przypadkach przydatne może być również zadeklarowanie ścisłych pól konstruktora .
  4. Możliwe jest także użycie strategii do wymuszenia oceny. Biblioteka strategii jest głównie ukierunkowana na obliczenia równoległe, ale ma również metody wymuszania wartości do WHNF ( rseq) lub pełnej NF ( rdeepseq). Istnieje wiele użytecznych metod pracy z kolekcjami, łączenia strategii itp.
Petr Pudlák
źródło
+1: Dziękujemy za przydatne wskazówki i linki. Punkt 3 wydaje się dość interesujący (i najłatwiejsze rozwiązanie dla mnie w tej chwili). Jeśli chodzi o sugestię 1, nie widzę, jak unikanie leniwego IO może poprawić rzeczy: O ile rozumiem, leniwe IO powinno być lepsze dla filtra, który ma przetwarzać (być może bardzo długi) strumień danych.
Giorgio
2
@Giorgio Dodałem link do Wiki Haskell o problemach z Lazy IO. Z leniwym We / Wy możesz mieć trudności z zarządzaniem zasobami. Na przykład, jeśli nie w pełni odczytałeś dane wejściowe (na przykład z powodu leniwej oceny), uchwyt pliku pozostaje otwarty . A jeśli ręcznie zamykasz uchwyt pliku, często zdarza się, że z powodu leniwego odczytu oceny jest on odkładany i zamykasz uchwyt przed odczytaniem całego wejścia. I często trudno jest uniknąć problemów z pamięcią z leniwym IO.
Petr Pudlák
Ostatnio miałem ten problem i w moim programie brakowało deskryptorów plików. Więc zastąpiłem leniwe IO ścisłym IO używając ścisłego ByteString.
Giorgio,