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ę.
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 typ f (a -> b) -> f a -> f b
, chcę operatora aplikacji z typem g a b -> a -> b
gdzie a
i b
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.