Em um problema de aprendizado com o qual estou brincando, percebi que precisava de uma classe para funções com operações para aplicação, composição etc. Razões ...
Pode ser conveniente tratar uma representação de uma função como se fosse a própria função, de modo que a aplicação implícita da função use um intérprete e a composição de funções deriva uma nova descrição.
Depois de ter uma classe para funções, você pode derivar classes para tipos especiais de funções - no meu caso, eu quero funções invertíveis.
Por exemplo, funções que aplicam deslocamentos de número inteiro podem ser representadas por um ADT contendo um número inteiro. Aplicar essas funções significa apenas adicionar o número inteiro. A composição é implementada adicionando os números inteiros quebrados. A função inversa tem o número inteiro negado. A função de identidade quebra zero. A função constante não pode ser fornecida porque não há representação adequada para ela.
É claro que não é necessário escrever as coisas como se os valores fossem funções genuínas de Haskell, mas depois que tive a ideia, pensei que uma biblioteca como essa já deveria existir e talvez até usar as grafias padrão. Mas não consigo encontrar essa classe na biblioteca Haskell.
Encontrei o módulo Data.Function , mas não há classe - apenas algumas funções comuns que também estão disponíveis no Prelude.
Então - por que não há uma classe para funções? É "apenas porque não existe" ou "porque não é tão útil quanto você pensa"? Ou talvez haja um problema fundamental com a ideia?
O maior problema possível que eu pensei até agora é que a aplicação de funções em funções reais provavelmente teria que ser especificada pelo compilador para evitar um problema de loop - para aplicar esta função, preciso aplicar a função de aplicação de funções, e para fazer isso eu preciso chamar a função application application, e para fazer isso ...
Mais pistas
Exemplo de código para mostrar o que estou buscando ...
{-# 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)
O aplicativo envolve um tipo de unificação em que os valores unificados não são iguais, mas estão relacionados por meio de funções invertíveis - lógica no estilo Prolog, mas com a = f(b)
restrições em vez de a = b
. A maior parte da composição resultará da otimização de uma estrutura de busca de união. A necessidade de inversos deve ser óbvia.
Se nenhum item em um conjunto unificado tiver um valor exato, um item específico poderá ser quantificado apenas em relação a outro item nesse conjunto unificado. É por isso que não quero usar funções "reais" - calculando esses valores relativos. Eu poderia abandonar todo o aspecto da função e apenas ter quantidades absolutas e relativas - provavelmente só preciso de números / vetores e (+)
- mas meu astronauta de arquitetura interna quer a diversão dele.
A única maneira de separar os links novamente é através do backtracking, e tudo é puro - a busca por união será feita usando as chaves em IntMap
"ponteiros". Eu tenho um trabalho simples de encontrar união, mas como ainda não adicionei as funções invertíveis, não faz sentido listá-lo aqui.
Razões pelas quais não posso usar o aplicativo, mônada, seta etc.
As principais operações que eu preciso que a classe de abstração da função forneça são aplicação e composição. Isso parece familiar - por exemplo Applicative
(<*>)
, Monad
(>>=)
e Arrow
(>>>)
são todas funções de composição. No entanto, os tipos que implementam a abstração da função no meu caso conterão alguma estrutura de dados que representa uma função, mas que não é (e não pode conter) uma função e que pode representar apenas um conjunto limitado de funções.
Como mencionei na explicação do código, às vezes só posso quantificar um item em relação a outro porque nenhum item em um cluster "unificado" tem um valor exato. Eu quero ser capaz de derivar uma representação dessa função, que em geral será a composição de várias funções fornecidas (subindo para um ancestral comum na árvore união / localização) e de várias funções inversas (voltando para a outra item).
Caso simples - onde as "funções" originais são limitadas às "funções" de deslocamento inteiro, quero o resultado composto como uma "função" de deslocamento inteiro - adicione as compensações do componente. Essa é uma parte importante do motivo pelo qual a função de composição precisa estar na classe e também na função de aplicativo.
Isso significa que não posso fornecer as operações pure
, return
ou arr
para os meus tipos, então não posso usar Applicative
, Monad
ou Arrow
.
Não é uma falha desses tipos - é uma incompatibilidade de abstrações. A abstração que eu quero é de uma simples função pura. Não há efeito colateral, por exemplo, e não é necessário criar uma notação conveniente para sequenciar e compor as funções que não sejam equivalentes ao padrão (.) Que se aplica a todas as funções.
Eu poderia instância Category
. Estou confiante de que todas as minhas coisas funcionais serão capazes de fornecer uma identidade, embora eu provavelmente não precise dela. Mas como Category
ele não suporta aplicativos, ainda assim eu precisaria de uma classe derivada para adicionar essa operação.
Applicative
esteja certo - ele exige que os valores sejam agrupados, assim como as funções, enquanto eu só quero agrupar as funções, e as funções agrupadas são realmente funções, enquanto minhas funções agrupadas normalmente não serão (em o caso mais geral, são ASTs que descrevem funções). Onde <*>
tem tipo f (a -> b) -> f a -> f b
, quero um operador de aplicativo com o tipo g a b -> a -> b
onde a
e b
especifique o domínio e o código da função agrupada, mas o que está dentro do wrapper não é (necessariamente) uma função real. No Arrows - possivelmente, vou dar uma olhada.