Algebraiczne typy danych i haskellowe rekordy

Kiedy w językach obiektowych potrzebujemy struktury, opisującej jakieś dane, tworzymy po prostu klasę. W Haskellu mamy zaś… algebraiczne typy danych. Ale dlaczego algebraiczne? Czy kryje się tutaj jakaś czyhająca na nas komplikacja? Przyjrzyjmy się tej sprawie i rozwikłajmy zagadkę haskellowych ADT (ang. Algebraic Data Types).

Dlaczego algebraiczne?

Mając do dyspozycji garść podstawowych typów danych, takich jak Bool, Char czy Int, możemy przystąpić do konstruowania własnych typów. Dajmy na to, taki oto typ, przechowujący imię, nazwisko i wiek osoby:

Posiada on jeden konstruktor – Person – zdefiniowany po prawej stronie znaku równości. Konstruktor ten przyjmuje trzy parametry. Typ ten moglibyśmy jednak opisać nieco inaczej, używając kilku konstruktorów, np.:

Na podstawie tych dwóch przykładów możemy już wyjaśnić sobie na czym polega owa algebraiczność haskellowych typów danych. Mamy tu do czynienia z dwoma rodzajami typów:

  • iloczynami (product types) – określanymi przez konstruktor i następujące po nim parametry
  • sumami (sum types) – składającymi się z co najmniej dwóch alternatywnych konstruktorów, oddzielonych od siebie znakami |

Warto przy tym zauważyc, że sumy mogą zawierać w sobie typy iloczynowe – tak jak ma to miejsce w drugim z naszych przykładów.

Parametry z nazwami

Jak na razie nie wygląda to zbyt czytelnie. Co z tego, że mamy zdefiniowany nasz algebraiczny typ danych, skoro opisujemy go jedynie przez podanie typów parametrów, a nie ich nazw? Skąd będziemy później wiedzieć który z parametrów String przechowuje imię, a który nazwisko?

Zanim przejdziemy do możliwych rozwiązań, zerknijmy jeszcze jak wygląda tworzenie nowych instancji naszego typu (tego składającego się z jednego konstruktora):

Jednym ze sposobów na pozbawione nazw parametry jest skorzystanie z tzw. rekordów:

Generowanie instancji takich typów może odbywać się albo tak jak poprzednio, albo też wraz z podaniem nazw parametrów:

Czyżby zatem Haskell oferował równie intuicyjne złożone typy danych jak wiele innych języków programowania? Otóż nie do końca. Po pierwsze, do tak zdefiniowanych pól nie odnosimy się, pisząc somePerson.name lub somePerson->name, ale po prostu wywołując funkcje:

Można by powiedzieć „ok, to tylko kwestia przyzwyczajenia”. Można, gdyby nie fakt, iż sprawa nieco się komplikuje, kiedy do tego samego pliku z haskellowym kodem dorzucimy jeszcze np. taki ATD:

Kompilacja niestety nie powiedzie się, a naszym oczom ukaże się błąd: Multiple declarations of ‘name'. Nie można po prostu zadeklarować dwóch funkcji o tej samej nazwie. Rozwiązań tej sytuacji jest kilka:

Modularyzacja

Podział kodu źródłowego na mniejsze fragmenty, znajdujące się w osobnych plikach, generalnie jest dobrym pomysłem. Stosując taką taktykę, unikniemy próby deklaracji kilku funkcji o tej samej nazwie we wspólnej przestrzenii nazw. Następnie, korzystając z import qualified (czyli importowania modułu w taki sposób, aby wszystkie jego funkcje trzeba było poprzedzać nazwą modułu i następującą po niej kropką) będziemy mogli odnosić się do obu funkcji, pisząc, w zależności od kontekstu Person.name lub Company.name. Oto zawartość poszczgólnych plików:

Klasy typów

Innym rozwiązaniem na „przeładowanie” funkcji jest wykorzystanie klas typów (ang. typeclasses). O samych klasach typów porozmawiamy sobie w jednym z kolejnych wpisów, więc nie przejmuj się, jeśli poniższe fragmenty kodu nie będą dla Ciebie do końca jasne. Aktualnie wystarczy nam wiedza, że jest to coś na podobieństwo znanych z programowania obiektowego interfejsów.

Pomysł polega na tym, że wewnątrz definicji rekordów będziemy używać unikalnych nazw parametrów, natomiast dzięki dodatkowej klasie zapewnimy również dostęp do nich przy pomocy funkcji o nieunikalnej nazwie:

Podobne podejście możemy zastosować również zupełnie rezygnując z rekordów, na rzecz zwykłych ATD:

Rozszerzenia, biblioteki

I w tym miejscu dochodzimy do podejrzenia, że rezygnacja z użycia rekordów może rzeczywiście jest dobrym pomysłem. Problem z kolizją funkcji dostępowych jest tylko jednym z wielu. Kłopotliwe są również chociażby identycznie nazwane parametry dla różnych konstruktorów tego samego typu

Zdaniem wielu użytkowników Haskella, rekordy są aktualnie jednym z jego słabych punktów. Możliwe, że problem zostanie wreszcie naprawiony w którymś z kolejnych standardów tego języka. Do tego czasu, skazani jesteśmy na stosowanie alternatywnych rozwiązań. Wśród nich znajdziemy zarówno dodatkowe moduły, jak i rozszerzenia języka (language extensions). Poniżej kilka przydatnych linków dla zainteresowanych tematem:

.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *