W problemach z nauką, z którymi się bawiłem, zdałem sobie sprawę, że potrzebuję kroju typowego dla funkcji z operacjami do aplikowania, komponowania itp. Powody ...
Wygodne może być traktowanie reprezentacji funkcji tak, jakby była ona samą funkcją, tak że zastosowanie funkcji niejawnie korzysta z interpretera, a komponowanie funkcji daje nowy opis.
Gdy masz już typ dla funkcji, możesz uzyskać pochodne typy dla specjalnych rodzajów funkcji - w moim przypadku chcę funkcji odwracalnych.
Na przykład funkcje, które stosują przesunięcia liczb całkowitych, mogą być reprezentowane przez ADT zawierający liczbę całkowitą. Zastosowanie tych funkcji oznacza po prostu dodanie liczby całkowitej. Kompozycja jest realizowana przez dodanie zawiniętych liczb całkowitych. Funkcja odwrotna ma zanegowaną liczbę całkowitą. Funkcja tożsamości otacza zero. Nie można zapewnić stałej funkcji, ponieważ nie ma dla niej odpowiedniej reprezentacji.
Oczywiście nie trzeba przeliterować rzeczy tak, jakby wartości były prawdziwymi funkcjami Haskella, ale kiedy wpadłem na ten pomysł, pomyślałem, że taka biblioteka musi już istnieć, a może nawet używać standardowych pisowni. Ale nie mogę znaleźć takiej klasy w bibliotece Haskell.
Znalazłem moduł Data.Function , ale nie ma żadnej klasy - tylko niektóre typowe funkcje, które są również dostępne z Preludium.
Więc - dlaczego nie ma klasy dla funkcji? Czy to „tylko dlatego, że nie ma” czy „ponieważ nie jest tak przydatne, jak myślisz”? A może pomysł ma fundamentalny problem?
Największym możliwym problemem, o jakim do tej pory myślałem, jest to, że aplikacja funkcji na rzeczywistych funkcjach prawdopodobnie musiałaby zostać specjalnie zaprojektowana przez kompilator, aby uniknąć problemu z zapętleniem - aby zastosować tę funkcję, muszę zastosować funkcję aplikacji funkcji, i aby to zrobić, muszę wywołać funkcję aplikacji funkcji, i aby to zrobić ...
Więcej wskazówek
Przykładowy kod pokazujący, do czego dążę ...
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
-- In my first version, Doable only had the one argument f. This version
-- seemed to be needed to support the UndoableOffset type.
--
-- It seems to work, but it also seems strange. In particular,
-- the composition function - a and b are in the class, but c isn't,
-- yet there's nothing special about c compared with a and b.
class Doable f a b where
fwdApply :: f a b -> a -> b
compDoable :: f b c -> f a b -> f a c
-- In the first version, I only needed a constraint for
-- Doable f a b, but either version makes sense.
class (Doable f a b, Doable f b a) => Undoable f a b where
bwd :: f a b -> f b a
bwdApply :: f a b -> b -> a
bwdApply f b = fwdApply (bwd f) b
-- Original ADT - just making sure I could wrap a pair of functions
-- and there were no really daft mistakes.
data UndoableFn a b = UFN { getFwd :: a -> b, getBwd :: b -> a }
instance Doable UndoableFn a b where
fwdApply = getFwd
compDoable f g = UFN ((getFwd f) . (getFwd g)) ((getBwd g) . (getBwd f))
instance Undoable UndoableFn a b where
bwd f = UFN (getBwd f) (getFwd f)
bwdApply = getBwd
-- Making this one work led to all the extensions. This representation
-- can only represent certain functions. I seem to need the typeclass
-- arguments, but also to need to restrict which cases can happen, hence
-- the GADT. A GADT with only one constructor still seems odd. Perhaps
-- surprisingly, this type isn't just a toy (except that the whole thing's
-- a toy really) - it's one real case I need for the exercise. Still a
-- simple special case though.
data UndoableOffset a b where
UOFF :: Int -> UndoableOffset Int Int
instance Doable UndoableOffset Int Int where
fwdApply (UOFF x) y = y+x
compDoable (UOFF x) (UOFF y) = UOFF (x+y)
instance Undoable UndoableOffset Int Int where
bwdApply (UOFF x) y = y-x
bwd (UOFF x) = UOFF (-x)
-- Some value-constructing functions
-- (-x) isn't shorthand for subtraction - whoops.
undoableAdd :: Int -> UndoableFn Int Int
undoableAdd x = UFN (+x) (\y -> y-x)
undoableMul :: Int -> UndoableFn Int Int
undoableMul x = UFN (*x) (`div` x)
-- With UndoableFn, it's possible to define an invertible function
-- that isn't invertible - to break the laws. To prevent that, need
-- the UFN constructor to be private (and all public ops to preserve
-- the laws). undoableMul is already not always invertible.
validate :: Undoable f a b => Eq a => f a b -> a -> Bool
validate f x = (bwdApply f (fwdApply f x)) == x
-- Validating a multiply-by-zero invertible function shows the flaw
-- in the validate-function plan. Must try harder.
main = do putStrLn . show $ validate (undoableAdd 3) 5
putStrLn . show $ validate (undoableMul 3) 5
--putStrLn . show $ validate (undoableMul 0) 5
fb1 <- return $ UOFF 5
fb2 <- return $ UOFF 7
fb3 <- return $ compDoable fb1 fb2
putStrLn $ "fwdApply fb1 3 = " ++ (show $ fwdApply fb1 3)
putStrLn $ "bwdApply fb1 8 = " ++ (show $ bwdApply fb1 8)
putStrLn $ "fwdApply fb3 2 = " ++ (show $ fwdApply fb3 2)
putStrLn $ "bwdApply fb3 14 = " ++ (show $ bwdApply fb3 14)
Aplikacja wymaga pewnego rodzaju unifikacji, w której zunifikowane wartości nie są równe, ale są powiązane poprzez te odwracalne funkcje - logika w stylu prologu, ale a = f(b)
raczej z ograniczeniami a = b
. Większość kompozycji będzie wynikać z optymalizacji struktury znajdowania związków. Potrzeba odwrotności powinna być oczywista.
Jeśli żaden element w zunifikowanym zestawie nie ma dokładnej wartości, to konkretny element może być określony ilościowo tylko w stosunku do innego elementu w tym zunifikowanym zestawie. Dlatego nie chcę używać „rzeczywistych” funkcji - obliczania tych względnych wartości. Mógłbym porzucić cały aspekt funkcji i po prostu mieć wielkości bezwzględne i względne - prawdopodobnie potrzebuję tylko liczb / wektorów i (+)
- ale mój astronauta z architektury wewnętrznej chce swojej zabawy.
Jedynym sposobem, w jaki ponownie rozbijam linki, jest cofanie się i wszystko jest czyste - szukanie związku zostanie wykonane przy użyciu kluczy IntMap
jako „wskaźników”. Mam proste wyszukiwanie związków, ale ponieważ nie dodałem jeszcze odwracalnych funkcji, nie ma sensu wymieniać go tutaj.
Powody, dla których nie mogę korzystać z Applicative, Monad, Arrow itp
Główne operacje, których potrzebuję, aby zapewnić klasę abstrakcji funkcji, to aplikacja i kompozycja. Brzmi to znajomo - EG Applicative
(<*>)
, Monad
(>>=)
i Arrow
(>>>)
są wszystkie funkcje kompozycji. Jednak typy, które implementują abstrakcję funkcji w moim przypadku będą zawierać pewną strukturę danych, która reprezentuje funkcję, ale która nie jest (i nie może zawierać) funkcję, i która może reprezentować tylko pewien ograniczony zestaw funkcji.
Jak wspomniałem w objaśnieniu kodu, czasami mogę tylko określić ilościowo jeden element względem drugiego, ponieważ żaden element w „zunifikowanym” klastrze nie ma dokładnej wartości. Chcę mieć możliwość przedstawienia reprezentacji tej funkcji, która na ogół będzie kompozycją kilku zapewnionych funkcji (podchodzenie do wspólnego przodka w drzewie unii / znalezienia) i kilku funkcji odwrotnych (chodzenie z powrotem do drugiej pozycja).
Prosty przypadek - gdy oryginalne „funkcje” ograniczają się do „funkcji” z przesunięciem całkowitym, chcę, aby złożony wynik był „funkcją” z przesunięciem całkowitym - dodaj przesunięcia komponentu. To duża część tego, dlaczego funkcja kompozycji musi znajdować się w klasie, podobnie jak funkcja aplikacji.
To znaczy, że nie może dostarczyć operacji pure
, return
lub arr
dla moich typów, więc nie mogę używać Applicative
, Monad
lub Arrow
.
To nie jest awaria tego typu - to niedopasowanie abstrakcji. Abstrakcja, której chcę, ma prostą, czystą funkcję. Nie ma na przykład efektów ubocznych i nie trzeba budować wygodnej notacji do sekwencjonowania i komponowania funkcji innych niż odpowiednik standardu (.), Który dotyczy wszystkich funkcji.
I może instancja Category
. Jestem pewien, że wszystkie moje funkcjonalne rzeczy będą w stanie zapewnić tożsamość, chociaż prawdopodobnie jej nie potrzebuję. Ponieważ jednak Category
nie obsługuje aplikacji, nadal potrzebowałbym klasy pochodnej, aby dodać tę operację.
źródło
Applicative
jest to całkiem słuszne - wymaga to zarówno pakowania wartości, jak i funkcji, podczas gdy chcę tylko zawijać funkcje, a funkcje zawinięte tak naprawdę są funkcjami, podczas gdy moje funkcje zawinięte zwykle nie są (w najbardziej ogólny przypadek to AST opisujące funkcje). Gdzie<*>
ma typf (a -> b) -> f a -> f b
, chcę operatora aplikacji z typemg a b -> a -> b
gdziea
ib
określ domenę i kododainę zawiniętej funkcji, ale to, co jest w opakowaniu, nie jest (koniecznie) prawdziwą funkcją. Na strzały - być może, rzuci mi okiem.Odpowiedzi:
Cóż, nie znam żadnych pomysłów wypiekanych na rynku jako reprezentujących rzeczy „funkcjonalne”. Ale jest kilka, które się zbliżają
Kategorie
Jeśli masz prostą koncepcję funkcji, która ma tożsamości i skład, niż masz kategorię.
Wadą jest to, że nie można stworzyć miłą kategorii wystąpienie z zestawem obiektów (
a
,b
, ic
). Możesz stworzyć niestandardową klasę kategorii, jak sądzę.Strzały
Jeśli twoje funkcje mają pojęcie o produktach i mogą wprowadzać dowolne funkcje, to strzałki są dla ciebie
ArrowApply
ma pojęcie aplikacji, które wydaje się ważne dla tego, czego chcesz.Wnioskodawcy
Wnioskodawcy mają twoje pojęcie o aplikacji, użyłem ich w AST do reprezentowania aplikacji funkcji.
Istnieje wiele innych pomysłów. Ale częstym tematem jest budowanie struktury danych reprezentującej twoją funkcję, a następnie przekazywanie jej do funkcji interpretacyjnej.
To także ile wolnych monad działa. Sugerowałbym szturchanie ich, jeśli czujesz się odważny, są one potężnym narzędziem do rzeczy, które sugerujesz, i zasadniczo pozwalają zbudować strukturę danych za pomocą
do
notacji, a następnie zwinąć ją do obliczeń efektów ubocznych z różnymi funkcjami . Ale piękno polega na tym, że te funkcje działają po prostu na strukturze danych i nie są tak naprawdę świadomi, jak to wszystko zrobili. Oto, co zasugerowałbym dla twojego przykładu tłumacza.źródło
($)
. Strzały na pierwszy rzut oka wyglądają jak ogromne przesady, ale wciążArrowApply
brzmią obiecująco - tak długo, jak nie muszę niczego zapewniać, nie mogę być w porządku. +1 na chwilę, z większą ilością do sprawdzenia.Applicative
lubArrow
(lubMonad
) - nie mogę zawinąć normalnej funkcji (ogólnie), ponieważ wartości mojego typu reprezentują funkcję, ale są reprezentowane przez dane i nie będą obsługiwać funkcji arbitralnych, jeśli jeśli istniał sposób na tłumaczenie. Oznacza to, że nie mogę zapewnićpure
,arr
anireturn
dla przypadków. BTW - te klasy są przydatne, ale nie mogę ich użyć do tego konkretnego celu.Arrow
nie jest „masywną przesadą” - to było fałszywe wrażenie po ostatniej próbie przeczytania artykułu, kiedy nie byłem gotowy go zrozumieć.Jak zauważyłeś, głównym problemem związanym z używaniem Aplikacji jest brak rozsądnej definicji
pure
. DlategoApply
został wynaleziony. Przynajmniej tak to rozumiem.Niestety nie mam pod ręką przykładów takich przypadków
Apply
też nie maApplicative
. Twierdzi się, że to prawdaIntMap
, ale nie mam pojęcia, dlaczego. Podobnie nie wiem, czy twój przykład - offset liczb całkowitych - dopuszczaApply
instancję.źródło
Oprócz wymienionych
Category
,Arrow
orazApplicative
:Odkryłem również
Data.Lambda
przez Conal Elliott:Wygląda interesująco, ale trudno go zrozumieć bez przykładów ...
Przykłady
Przykłady można znaleźć na stronie wiki o wartościach materialnych (TV), które wydają się być jedną z rzeczy, które spowodowały utworzenie
TypeCompose
biblioteki; patrz Wejścia i wartościowe wartości wyjściowe .Ideą biblioteki telewizyjnej jest wyświetlanie wartości Haskella (w tym funkcji) w namacalny sposób.
Aby postępować zgodnie z regułą StackOverflow dotyczącą nie publikowania gołych samotników, kopiuję kilka bitów poniżej, które powinny dać wyobrażenie o tych rzeczach:
Pierwszy przykład brzmi:
co daje, gdy jest uruchomiony jako
runIO shopping
(zobacz tam więcej komentarzy, GUI i więcej przykładów):źródło
Data.Lambda
są klasy dla rzeczy podobnych do funkcji (o które poproszono) ... Nie byłem pewien, jak te rzeczy mają być używane. Trochę to zbadałem. Prawdopodobnie nie zapewniają one jednak abstrakcji dla aplikacji funkcji.