Obejście dotyczące wdrażania operacji na podwójnie połączonych lub cyklicznych strukturach danych w językach z danymi niezmiennymi

11

Chciałbym dowiedzieć się, jak tworzyć wykresy i wykonywać na nich pewne lokalne operacje w Haskell, ale pytanie nie jest specyficzne dla Haskell i zamiast wykresów możemy rozważyć podwójnie połączone listy.

Pytanie: Jaki byłby idiomatyczny lub zalecany sposób wdrożenia podwójnie powiązanej listy (lub innej podwójnie powiązanej lub okrągłej struktury danych) i operacji na niej w języku, który głównie obsługuje i zaleca niezmienne struktury danych (Haskell, Clojure itp.) ? W szczególności, jak korzystać z aktualizacji lokalnych, które są formalnie zabronione przez język?

Mogę łatwo wyobrazić sobie, że jeśli jakieś operacje lokalne są wykonywane na podwójnie połączonej liście (na przykład, jeśli wstawiony jest element), może nie być konieczne kopiowanie całej listy natychmiast z powodu lenistwa języka. Ponieważ jednak lista jest podwójnie połączona, jeśli zostanie zmodyfikowana w jednym miejscu, żaden ze starych węzłów nie będzie mógł zostać użyty w nowej wersji listy, a prędzej czy później trzeba by je jakoś oznaczyć, skopiować, wyrzucić śmieci. . Oczywiście są to operacje zbędne, jeśli zostanie użyta tylko zaktualizowana kopia listy, ale dodają „koszty ogólne” proporcjonalne do wielkości listy.

Czy to oznacza, że ​​do takich zadań niezmienne dane są po prostu nieodpowiednie, a funkcjonalne języki deklaratywne bez „natywnej” obsługi danych zmiennych nie są tak dobre, jak te niezbędne? Czy jest jakieś trudne obejście?

PS Znalazłem kilka artykułów i prezentacji na ten temat w Internecie, ale miałem trudności z ich śledzeniem, podczas gdy myślę, że odpowiedź na to pytanie nie powinna zająć więcej niż jednego akapitu i może diagramu ... to znaczy, jeśli jest nie ma „funkcjonalnego” rozwiązania tego problemu, odpowiedź prawdopodobnie brzmi „użyj C”. Jeśli istnieje, to jak skomplikowane może być?


Powiązane pytania


Odpowiednie cytaty

Czysto funkcjonalne języki programowania umożliwiają bardzo zwięzłe wyrażanie wielu algorytmów, ale istnieje kilka algorytmów, w których zdatny do aktualizacji stan wydaje się odgrywać kluczową rolę. W przypadku tych algorytmów języki czysto funkcjonalne, które nie mają stanu umożliwiającego aktualizację, wydają się z natury nieefektywne ( [Ponder, McGeer i Ng, 1988] ).

- John Launchbury i Simon Peyton Jones, Lazy wątki stanu funkcjonalnego (1994), a także John Launchbury i Simon Peyton Jones, stan w Haskell (1995). Te prace przedstawiają STkonstruktor typu monadycznego w Haskell.

Alexey
źródło
4
Zalecane: Okasaki
Robert Harvey
2
Dzięki za referencje. Znalazłem jego pracę magisterską .
Alexey,
Ten artykuł wygląda obiecująco: Leniwe algorytmy głębokiego wyszukiwania i wykresów liniowych w Haskell (1994), autorstwa Davida Kinga i Johna Launchbury.
Alexey,
Wygląda na to, że podobny problem z tablicami rozwiązuje pakiet diffarray , który implementuje DiffArraytyp. Patrząc na źródło z diffarray pakietu, widzę 91 wystąpień unsafePerformIO. Wygląda na to, że odpowiedź na moje pytanie brzmi „tak, nie, czysto funkcjonalne języki z niezmiennymi danymi nie są odpowiednie do implementacji algorytmów, które zwykle polegają na aktualizacjach w miejscu”.
Alexey,
Moje obecne rozwiązanie (w Haskell) polega na użyciu słownika ( Map, IntMaplub HashMap) jako pamięci i tworzeniu węzłów zawierających identyfikatory połączonych węzłów. „Wszystkie problemy w informatyce można rozwiązać przez inny poziom pośredni”.
Alexey,

Odpowiedzi:

6

Mogą istnieć inne wydajne niezmienne struktury danych, które pasują do konkretnego zadania, ale nie są tak ogólne jak podwójnie połączona lista (która niestety jest podatna na błędy w modyfikacji modyfikacji ze względu na jej zmienność). Jeśli zawęzisz swój problem, prawdopodobnie znajdziesz taką strukturę.

Ogólną odpowiedzią na (względnie) ekonomiczne przechodzenie niezmiennych struktur są soczewki. Chodzi o to, że możesz zachować wystarczającą ilość informacji, aby zrekonstruować zmodyfikowaną niezmienną strukturę z jej niezmodyfikowanych części i aktualnie zmodyfikowanego elementu, i nawigować nad nią do sąsiedniego węzła.

Kolejną przydatną strukturą jest zamek błyskawiczny . (Zabawne jest to, że podpis typu dla zamka błyskawicznego do obiektywu jest pochodną matematyki szkoły od podpisu typu struktury).

Oto kilka linków.

9000
źródło
1
w zależności od potrzeb może być również przydatny zamek błyskawiczny
jk.
Aby zawęzić mój problem, załóżmy, że chcę zaprogramować system przepisywania grafów, na przykład kalkulator lambda oparty na przepisywaniu grafów.
Alexey,
1
@Alexey: Czy znasz pracę Czystych ludzi przy przepisywaniu grafów? wiki.clean.cs.ru.nl/…
Giorgio
1
@Alexey: Nie wiem, że: Clean jest kuzynem Haskell, który został opracowany samodzielnie. Ma również inny mechanizm radzenia sobie z efektami ubocznymi (AFAIK nazywa się to unikalnymi typami). Z drugiej strony programiści dużo pracowali przy przepisywaniu grafów. Mogą więc należeć do najlepszych ludzi, którzy wiedzą zarówno o przepisywaniu grafów, jak i programowaniu funkcjonalnym.
Giorgio,
1
Zgadzam się, że zamek błyskawiczny wydaje się rozwiązać problem z podwójnie połączoną listą lub drzewem, jeśli chcę nawigować i modyfikować w miejscu, w którym aktualnie jestem, ale nie jest jasne, co zrobić, jeśli chcę skupić się na kilku miejscach jednocześnie i na przykład zamień dwa elementy w dwóch odległych od siebie miejscach. Jeszcze mniej jasne jest, czy można go stosować ze strukturami „okrągłymi”.
Alexey,
2

Haskell nie zabrania używania zmiennych struktur danych. Są one mocno zniechęcone i trudniejsze w użyciu, ponieważ części kodu, które ich używają, muszą ostatecznie zwrócić akcję IO (która ostatecznie musi być powiązana z akcją IO zwracaną przez funkcję główną), ale to nie oznacza uniemożliwić korzystanie z takich struktur, jeśli naprawdę ich potrzebujesz.

Sugerowałbym zbadanie wykorzystania programowej pamięci transakcyjnej jako drogi naprzód. Zapewnia nie tylko skuteczny sposób implementacji struktur zmiennych, ale także zapewnia bardzo przydatne gwarancje bezpieczeństwa wątków. Zobacz opis modułu na https://hackage.haskell.org/package/stm i przegląd wiki na https://wiki.haskell.org/Software_transactional_memory .

Jules
źródło
Dzięki, postaram się dowiedzieć o STM. Wygląda na to, że jest więcej metod w Haskell mieć zmienność i stan (mam natyka MVar, State, ST), więc będę musiał zorientować się ich różnic i planowanych zastosowań.
Alexey,
@Alexey: Dobra uwaga odnośnie do STIMO, należy o tym wspomnieć w odpowiedzi, ponieważ pozwala ona uruchomić obliczenia stanowe, a następnie wyrzucić stan i wyodrębnić wynik jako czystą wartość.
Giorgio,
@Giorgio, czy można używać Haskell's STz STM, aby mieć zarówno współbieżność, jak i stan jednorazowy?
Alexey,
Jeszcze jedna sugestia terminologiczna: złożone działanie IO nie jest „ zwracane przez funkcję główną”, ale jest przypisywane do mainzmiennej. :) ( mainnawet nie pełni żadnej funkcji)
Alexey
Widzę twój punkt widzenia, ale wciąż „zmienna” ma w umysłach większości ludzi konotację jako prostą wartość, a nie proces, który generuje wartość, a główna jest wyraźnie lepsza jako druga. Sugerowana przez Ciebie zmiana, choć wyraźnie poprawna technicznie, może wprowadzić w błąd osoby niezaznajomione z tematem.
Jules