2015-08-01 21 views
12

Czytałem niektóre wyjaśnienia rodzajów algebraiczne danych:Dlaczego potrzebujemy "algebraicznych typów danych"?

Te artykuły dają bardzo szczegółowy opis i kod próbek.

Początkowo myślałem, że Algebraic Data Type służy do łatwego definiowania niektórych typów i możemy je dopasować do dopasowania wzorców. Ale po przeczytaniu tych artykułów okazało się, że "dopasowanie do wzorca" nawet tam nie jest wymieniane, a zawartość wygląda interesująco, ale o wiele bardziej złożona, niż się spodziewałem.

Więc mam kilka pytań (które odpowiedzi nie ma w tych artykułach):

  • Dlaczego potrzebujemy go, powiedzmy, w Haskell lub Scala?
  • Co możemy zrobić, jeśli ją mamy i czego nie możemy zrobić, jeśli jej nie mamy?
+3

Chcesz, aby krotki zwracały więcej niż jeden wynik, a chcesz, aby typy sumowały wybory, które wyraźnie zaznaczyłeś. (oczywiście jest to tylko pierwsza kropla w oceanie - możesz pisać książki o tych rzeczach). Link, który podałeś, może być mylący - możesz mieć pomysł, że potrzebujesz tych rzeczy tylko do wieżyczek z kości słoniowej-arkane-magii, ale jest to po prostu sposób na zarządzanie danymi w rozsądny sposób (szczególnie, jeśli nie masz klasy i odniesienia do dziedziczenia i mutacji do twojej dyspozycji) – Carsten

+0

@Carsten Czuję, że rozumiesz, z czym jestem zdezorientowany. Doceniłem twój komentarz i mam nadzieję, że możesz powiedzieć coś więcej :) – Freewind

+5

Wystarczy, że wyrazisz ostrzeżenie, że akronim "ADT" jest używany również w znaczeniu "Typ danych abstrakcyjnych", co jest przeciwieństwem pojęcia "Typ danych algebraicznych". Akronim "ADT" powinien zatem być używany z ostrożnością, tj. Wcale. – pigworker

Odpowiedz

9

Powinniśmy zacząć od wiki artykule Haskell Algebraic Data Types

i tu, na krótko, tylko moją wizję:

  • musimy je modelować logikę biznesową w starym obiektowego sposób (a właściwie w sposób oparty na starych kategoriach) i sprawić, że będzie on bardziej odporny na uszkodzenia, ponieważ kompilator może sprawdzić, czy dopasował wszystkie możliwe opcje. Lub, innymi słowy, że twoja funkcja to total, a nie częściowa. Ostatecznie daje kompilatorowi możliwość sprawdzenia poprawności kodu (dlatego zalecane są zapieczętowane cechy). Tak więc, im więcej typów masz, tym lepiej - ogólne programowanie pomaga ci tutaj, ponieważ produkuje więcej typów.
  • cechy standardowe: możemy przedstawić typ jako "zestaw" obiektów, możemy dopasować obiekt do typu (używając dopasowywania wzorca), a nawet zdekonstruować (przeanalizować) go za pomocą dopasowań. Możemy również dynamicznie dodawać zachowania (w czasie kompilacji) do tego typu z klasami typów. Jest to również możliwe dla zwykłej klasy, ale tutaj daje nam możliwość oddzielenia algebraicznego modelu (typy) od zachowania (funkcje)
  • możemy konstruować typy jako products/coproducts różnych obiektów/typów. Możesz myśleć o systemie algebraicznym jako zestawie (lub bardziej ogólnie - jako kategorii zamkniętej kartezjańsko). type Boolean = True | False oznacza, że ​​Boolean jest związkiem (współproduktem) z True i False. Cat(height: Height, weight: Weight) to "krotka" (ogólniej - produkt) z Height i Weight. Produkt (mniej więcej) reprezentuje relację "części" z OOP, coproduct - "is" (ale w odwrotny sposób).

To również daje nam sposób wysłać powierzony zachowania w czasie wykonywania w multimethod stylu (jak to było w CLOS):

sealed trait Animal 
    case class Cat extends Animal 
    case class Dog extends Animal 

def move(c: Animal) = c match { 
    case Cat(...) => ... 
    case Dog(...) => 
    case a: Animal => ...//if you need default implementation 
} 

Haskell, takich jak:

data Animal = Dog | Cat //coproduct 

let move Dog d = ... 
let move Cat c = ... 

Zamiast:

trait Animal { 
    ... 
    def move = ... 
} 

class Cat(val ...) extends Animal { 
    override def move = ... 
} 

class Dog(val ...) extends Animal { 
    override def move = ... 
} 

PS Teoretycznie, jeśli modelujesz świat w sposób algebraiczny, a twoje funkcje są kompletne i czyste - możesz udowodnić, że twoja aplikacja jest poprawna. Jeśli się kompiluje - działa :).

P.S.2. Powinienem też wspomnieć, że hierarchia typu Scala, która ma Any, nie jest tak dobra dla typesafety (ale dobra dla współpracy z Javą i IO), ponieważ łamie ładną strukturę zdefiniowaną przez GADTs. Więcej niż to case class może być zarówno GADT (algebraiczne) i ADT (streszczenie), a także zmniejsza gwarancje.

+0

Dzięki , właśnie to myślę ADT przed przeczytaniem tych artykułów. Ale wydaje się, że rozmawiają z czymś innym – Freewind

+0

Sądzę, że w rzeczywistości jest prawie taki sam, jak myślałeś, z wyjątkiem tego, że możesz (nie mniej) myśleć o nich jako o zestawach danych Boolean = True | Fałsz "oznacza, że ​​Boolean jest związkiem (współproduktem) True i False. Kot (wysokość: wysokość, waga: waga) jest produktem wysokości i wagi – dk14

+0

@Freewind Dodałem kilka analogii do teorii kategorii – dk14

2

Wpis na blogu, o którym wspomniałeś, dotyczy bardziej matematycznych aspektów typów danych algebraicznych, dotyczących ich praktycznego zastosowania w programowaniu. Myślę, że większość funkcjonalnych programistów nauczyła się najpierw o typach danych algebraicznych, używając ich w pewnym języku programowania, a dopiero później badała ich właściwości algebraiczne.

Rzeczywiście, intencją blogu jest jasne od samego początku:

w tej serii postów Wytłumaczę dlaczego typy danych Haskell są nazwie algebraicznych - bez wymieniania teorii kategorii lub zaawansowanych matematyka.

W każdym razie praktyczna użyteczność typów algebraicznych jest najlepiej doceniana podczas zabawy z nimi.

Załóżmy na przykład, że chcesz napisać funkcję przecinającą dwa segmenty na płaszczyźnie.

def intersect(s1: Segment, s2: Segment): ??? 

Jaki powinien być wynik? To kuszące, aby napisać:

def intersect(s1: Segment, s2: Segment): Point 

, ale co, jeśli nie ma skrzyżowania? Można próbować załatać tę skrzynkę narożną zwracając null lub rzucając wyjątek NoIntersection. Jednak dwa segmenty mogą również nakładać się na więcej niż jeden punkt, gdy leżą na tej samej linii prostej. Co powinniśmy robić w takich przypadkach? Zgłaszanie innego wyjątku?

Podejście algebraiczne typy jest zaprojektowanie rodzaj obejmujący wszystkie przypadki:

sealed trait Intersection 
case object NoIntersection extends Intersection 
case class SinglePoint(p: Point) extends Intersection 
case class SegmentPortion(s: Segment) extends Intersection 

def intersect(s1: Segment, s2: Segment): Intersection 

Istnieje wiele praktycznych przypadków, gdy takie podejście czuje się zupełnie naturalne. W niektórych innych językach bez typów algebraicznych należy odwoływać się do wyjątków, do null s (zobacz także the billion-dollar mistake), do klas niezamkniętych (uniemożliwiając kompilatorowi sprawdzenie kompletności dopasowywania wzorca) lub do innych funkcji przez język. Prawdopodobnie "najlepszą" opcją w OOP jest użycie Visitor pattern do kodowania typów algebraicznych i dopasowywania wzorców w językach, które nie mają takich funkcji.Mimo to posiadanie tego bezpośrednio wspieranego w języku, tak jak w scala, jest znacznie wygodniejsze.