Alternatywy dla Prelude w Haskellu

7 minut(y)

Haskell jest pięknym językiem programowania, ale nie jest doskonały. Haskell posiada wiele błędów projektowych. Biblioteka standardowa Haskella też nie jest idealna. Prelude jest zbiorem domyślnie importowanych modułów w Haskellu z biblioteki standardowej. Niestety prelude importuje wiele niebezpiecznych oraz powolnych funkcji. Jednocześnie nie importuje wielu użytecznych funkcji.

Dlatego alternatywy dla prelude próbują to poprawić. Są to między innymi biblioteki:

Jednak którą bibliotekę wybrać?

  • Tą, która ma najwięcej gwiazdek na githubie?
  • Tą, która ma najlepszą dokumentację?
  • Tą, która ma najlepszą stronę?

Nie jest to ważne. I tak we wszystkich trzech konkurencjach wygrywa relude.

Biblioteka relude

Biblioteka relude posiada wiele zalet, są to między innymi:

  • Programowanie totalne
  • Wydajność
  • Wygoda
  • Minimalizm

Programowanie totalne

Funkcje powinny być totalne. To znaczy dla każdego zestawu parametrów zwracać jakąś wartość. Jeśli funkcja dla jakichś argumentów nie zwraca wartości, tylko rzuca wyjątek, to jest częściowa, a nie totalna. Wiele funkcji z Prelude rzuca wyjątkami. Biblioteka relude posiada totalne wersje tych funkcji.

Osiąga to za pomocą:

  • funkcji zwracających Maybe lub Either zamiast zgłaszających błędy.
  • typu NonEmpty zamiast List tam, gdzie ma to sens.
  • nie importowania niebezpiecznych funkcji ja !!x.

Dalej można korzystać z niebezpiecznych funkcji, jeśli zaimportuje się moduł Unsafe:

import qualified Relude.Unsafe as Unsafe

Wydajność

Domyślnie w Haskellu literały tekstowe są typu String, gdzie String to lista znaków:

type String = [Char]

Jest to powolna implementacja napisów i zamiast String należy używać typu Text. Można to zmienić za pomocą przełącznika dla kompilatora ghc-options: -XOverloadedStrings. Można też użyć pragmy (dyrektywy?) kompilatora {-# LANGUAGE OverloadedStrings #-}, ale przełącznik jest lepszym rozwiązaniem, ponieważ działa we wszystkich plikach.

Następnie należy zastąpić wszystkie ewentualne łączenia Stringów za pomocą operatora ++, operatorem <>. W zasadzie to wszelkie łączenia list operatorem ++ można zastąpić operatorem łączenia monoidów <>.

Niestety Text znajduje się w module zewnętrznym text a nie module base będącym biblioteką standardową. Text jest zalecany, ale mimo to w Haskellu wiele funkcji z biblioteki standardowej pobiera lub zwraca typ String.

Biblioteka relude naprawia ten problem i tak:

  • show jest polimorficzny ze względu na typ zwracany, czyli może wracać zarówno String jak i Text.
  • funkcja error przyjmuje Text.

Dodatkowo mamy miły zestaw funkcji (toText|toLText|toString) do konwersji między typami. Dla typu ByteString jest o wiele prostsza do zapamiętania para funkcji encodeUtf8|decodeUtf8.

Połączenie tego wszystkiego (przełącznik -XOverloadedStrings, funkcja show, funkcja toText lub toString) może sprawić, że czasem Haskell nie będzie w stanie wywnioskować typu. Wszędzie tam, gdzie typ jest obojętny powinniśmy używać typu Text. A wszędzie tam gdzie typ jest trudny do wywnioskowania dla kompilatora Haskella musimy jawnie podać typ:

"Awesome relude!"::Text

Niestety funkcje readMaybe i readEither przyjmują dalej String zamiast Text. Powoduje to, że w niektórych przypadkach dalej lepiej używać Stringa:

readOrError :: Read a => String -> a
readOrError raw = check $ readEither raw where
  check (Right result) = result
  check (Left message) = error $ message <> " [" <> toText raw <> "]"

Wygoda

Biblioteka relude skraca czas pisania aplikacji, ponieważ wiele importów dzieje się automatycznie. Są to:

  • base
  • bytestring
  • containers
  • deepseq
  • ghc-prim
  • hashable
  • mtl
  • stm
  • text
  • transformers
  • unordered-containers

Modułów tych nie należy dodawać do zależności samodzielnie, tylko używać ich za pośrednictwem relude. Dzięki temu można usunąć z projektu importy wielu modułów. W moim przypadku było to:

import Control.Applicative
import Control.Monad.Except
import Data.Char
import Data.Map.Strict
import Data.Maybe
import Data.Text
import Numeric.Natural
import Text.Read

Dodatkowo kolejne funkcjonalności można importować za pomocą import Relude.Extra. Mnie przydały się funkcje lookup, next i prev.

Minimalizm

Biblioteka relude stara się być biblioteką minimalistyczną, czyli zależącą od minimalnej ilości modułów zewnętrznych. Jest to trudne, ponieważ Haskell posiada bardzo małą bibliotekę standardową base. O wiele za małą.

Zależności używane przez relude znajdują się na wykresie. Początkowo może się wydawać, że wykres zawiera pół internetu, ale zależności można podzielić na cztery grupy:

  • text i bytestring, czyli obsługa tekstu.
  • unordered-containers i containers, czyli kontenery (kolekcje). Przy czym containers zostało już dodane przez text.
  • mtl czyli transformatory monad (ContT, ExceptT, ListT, RWST, ReaderT, StateT, WriterT) potrzebne każdemu, kto chce się zmierzyć z zagnieżdżonymi monadami i stanem.
  • stm czyli legendarna Software Transactional Memory to pamięć transakcyjna używana przy współbieżności, która zainspirowała między innymi twórcę języka Clojure.

Czy to dużo? W porównaniu z biblioteką standardową wielu innych języków programowania powiedziałbym, że to bardzo mało.

Dokumentacja

Biblioteka relude posiada bardzo dobrą dokumentację i przewodnik przeprowadzający przez podmianę biblioteki.

Dodatkową zaletą jest linter hlint. Linter hlint jest narzędziem magicznym. Pozwala napisać kod, którego nie jesteśmy w stanie zrozumieć, przynajmniej na początku. Piszemy kod prosty i długi przy użyciu tylko trywialnych funkcji, a hlint podpowiada jak go skrócić i jednocześnie go skomplikować.

Podsumowanie

Czy obietnice składane przez twórców relude są spełnione? Jeszcze nie poznałem wszystkich zalet relude, ale uważam, że tak.

Na githubie znajduje się kod projektów [Helcam] i Helpa po użyciu biblioteki relude.