Dans les langages fonctionnels purs comme Haskell, existe-t-il un algorithme pour obtenir l'inverse d'une fonction, (modifier) lorsqu'elle est bijective? Et existe-t-il un moyen spécifique de programmer votre fonction?
Dans certains cas, oui! Il y a un beau papier appelé Bidirectionalization for Free! qui traite de quelques cas - lorsque votre fonction est suffisamment polymorphe - où il est possible, de manière complètement automatique de dériver une fonction inverse. (Il explique également ce qui rend le problème difficile lorsque les fonctions ne sont pas polymorphes.)
Ce que vous sortez dans le cas où votre fonction est inversible est l'inverse (avec une entrée parasite); dans d'autres cas, vous obtenez une fonction qui essaie de "fusionner" une ancienne valeur d'entrée et une nouvelle valeur de sortie.
Non, ce n'est pas possible en général.
Preuve: considérer les fonctions bijectives de type
type F = [Bit] -> [Bit]
avec
data Bit = B0 | B1
Supposons que nous ayons un onduleur inv :: F -> F
Tel que inv f . f ≡ id
. Supposons que nous l'avons testé pour la fonction f = id
, En confirmant que
inv f (repeat B0) -> (B0 : ls)
Étant donné que ce premier B0
Dans la sortie doit être venu après un certain temps, nous avons une limite supérieure n
à la fois sur la profondeur à laquelle inv
avait effectivement évalué notre entrée de test pour obtenir ce résultat, ainsi que le nombre de fois qu'il a pu appeler f
. Définissez maintenant une famille de fonctions
g j (B1 : B0 : ... (n+j times) ... B0 : ls)
= B0 : ... (n+j times) ... B0 : B1 : ls
g j (B0 : ... (n+j times) ... B0 : B1 : ls)
= B1 : B0 : ... (n+j times) ... B0 : ls
g j l = l
Clairement, pour tous 0<j≤n
, g j
Est une bijection, en fait auto-inverse. Nous devrions donc pouvoir confirmer
inv (g j) (replicate (n+j) B0 ++ B1 : repeat B0) -> (B1 : ls)
mais pour y parvenir, inv (g j)
aurait dû soit
g j (B1 : repeat B0)
à une profondeur de n+j > n
head $ g j l
pour au moins n
différentes listes correspondant à replicate (n+j) B0 ++ B1 : ls
Jusque-là, au moins l'un des g j
Ne peut être distingué de f
, et puisque inv f
N'avait effectué aucune de ces évaluations, inv
pouvait probablement pas fait la différence - à moins de faire quelques mesures d'exécution par lui-même, ce qui n'est possible que dans le IO Monad
.
⬜
Vous pouvez le consulter sur wikipedia, il s'appelle Reversible Computing .
En général, vous ne pouvez pas le faire et aucun des langages fonctionnels n'a cette option. Par exemple:
f :: a -> Int
f _ = 1
Cette fonction n'a pas d'inverse.
Pas dans la plupart des langages fonctionnels, mais dans la programmation logique ou la programmation relationnelle, la plupart des fonctions que vous définissez ne sont en fait pas des fonctions mais des "relations", et celles-ci peuvent être utilisées dans les deux sens. Voir par exemple prologue ou kanren.
Si vous pouvez énumérer le domaine de la fonction et comparer les éléments de la plage pour l'égalité, vous pouvez - d'une manière assez simple. Par énumérer, je veux dire avoir une liste de tous les éléments disponibles. Je m'en tiendrai à Haskell, car je ne connais pas Ocaml (ni même comment le capitaliser correctement ;-)
Ce que vous voulez faire, c'est parcourir les éléments du domaine et voir s'ils sont égaux à l'élément de la plage que vous essayez d'inverser, et prendre le premier qui fonctionne:
inv :: Eq b => [a] -> (a -> b) -> (b -> a)
inv domain f b = head [ a | a <- domain, f a == b ]
Puisque vous avez déclaré que f
est une bijection, il y a forcément un et un seul élément de ce type. L'astuce, bien sûr, est de s'assurer que votre énumération du domaine atteint réellement tous les éléments dans un temps fini . Si vous essayez d'inverser une bijection de Integer
à Integer
, l'utilisation de [0,1 ..] ++ [-1,-2 ..]
Ne fonctionnera pas car vous n'obtiendrez jamais les nombres négatifs. Concrètement, inv ([0,1 ..] ++ [-1,-2 ..]) (+1) (-3)
ne donnera jamais de valeur.
Cependant, 0 : concatMap (\x -> [x,-x]) [1..]
fonctionnera, car elle parcourt les entiers dans l'ordre suivant [0,1,-1,2,-2,3,-3, and so on]
. En effet, inv (0 : concatMap (\x -> [x,-x]) [1..]) (+1) (-3)
renvoie rapidement -4
!
Le paquet Control.Monad.Omega peut vous aider à parcourir les listes de tuples, etc. d'une bonne manière; Je suis sûr qu'il y a plus de paquets comme ça - mais je ne les connais pas.
Bien sûr, cette approche est plutôt sourde et brutale, sans parler de laide et inefficace! Je terminerai donc par quelques remarques sur la dernière partie de votre question, sur la façon "d'écrire" des bijections. Le système de type de Haskell n'est pas en mesure de prouver qu'une fonction est une bijection - vous voulez vraiment quelque chose comme Agda pour cela - mais il est prêt à vous faire confiance.
(Attention: le code non testé suit)
Vous pouvez donc définir un type de données de Bijection
s entre les types a
et b
:
data Bi a b = Bi {
apply :: a -> b,
invert :: b -> a
}
avec autant de constantes (où vous pouvez dire 'je sais ce sont des bijections!') comme vous le souhaitez, comme:
notBi :: Bi Bool Bool
notBi = Bi not not
add1Bi :: Bi Integer Integer
add1Bi = Bi (+1) (subtract 1)
et quelques combinateurs intelligents, tels que:
idBi :: Bi a a
idBi = Bi id id
invertBi :: Bi a b -> Bi b a
invertBi (Bi a i) = (Bi i a)
composeBi :: Bi a b -> Bi b c -> Bi a c
composeBi (Bi a1 i1) (Bi a2 i2) = Bi (a2 . a1) (i1 . i2)
mapBi :: Bi a b -> Bi [a] [b]
mapBi (Bi a i) = Bi (map a) (map i)
bruteForceBi :: Eq b => [a] -> (a -> b) -> Bi a b
bruteForceBi domain f = Bi f (inv domain f)
Je pense que vous pourriez alors faire invert (mapBi add1Bi) [1,5,6]
et obtenir [0,4,5]
. Si vous choisissez vos combinateurs de manière intelligente, je pense que le nombre de fois que vous devrez écrire une constante Bi
à la main pourrait être assez limité.
Après tout, si vous savez qu'une fonction est une bijection, vous aurez, espérons-le, une esquisse de preuve de ce fait dans votre tête, que l'isomorphisme de Curry-Howard devrait pouvoir transformer en programme :-)
De telles tâches sont presque toujours indécidables. Vous pouvez avoir une solution pour certaines fonctions spécifiques, mais pas en général.
Ici, vous ne pouvez même pas reconnaître quelles fonctions ont un inverse. Citant Barendregt, H. P. The Lambda Calculus: Its Syntax and Semantics. North Holland, Amsterdam (1984) :
Un ensemble de termes lambda n'est pas trivial s'il n'est ni l'ensemble vide ni l'ensemble complet. Si A et B sont deux ensembles non triviaux et disjoints de termes lambda fermés sous l'égalité (bêta), alors A et B sont récursivement inséparables.
Prenons A pour être l'ensemble des termes lambda qui représentent des fonctions inversibles et B le reste. Les deux sont non vides et fermés sous égalité bêta. Il n'est donc pas possible de décider si une fonction est inversible ou non.
(Cela s'applique au calcul lambda non typé. TBH Je ne sais pas si l'argument peut être directement adapté à un calcul lambda typé lorsque nous connaissons le type d'une fonction que nous voulons inverser. Mais je suis sûr que ce sera similaire.)
Toutes les fonctions n'ont pas d'inverse. Si vous limitez la discussion aux fonctions biunivoque, la possibilité d'inverser une fonction arbitraire permet de casser n'importe quel cryptosystème. Nous devons en quelque sorte espérer que ce n'est pas possible, même en théorie!
Non, toutes les fonctions n'ont même pas d'inverses. Par exemple, quel serait l'inverse de cette fonction?
f x = 1
Dans certains cas, il est possible de trouver l'inverse d'une fonction bijective en la convertissant en une représentation symbolique. Basé sur cet exemple , j'ai écrit ce programme Haskell pour trouver des inverses de quelques fonctions polynomiales simples:
bijective_function x = x*2+1
main = do
print $ bijective_function 3
print $ inverse_function bijective_function 3
data Expr = X | Const Double |
Plus Expr Expr | Subtract Expr Expr | Mult Expr Expr | Div Expr Expr |
Negate Expr | Inverse Expr |
Exp Expr | Log Expr | Sin Expr | Atanh Expr | Sinh Expr | Acosh Expr | Cosh Expr | Tan Expr | Cos Expr |Asinh Expr|Atan Expr|Acos Expr|Asin Expr|Abs Expr|Signum Expr|Integer
deriving (Show, Eq)
instance Num Expr where
(+) = Plus
(-) = Subtract
(*) = Mult
abs = Abs
signum = Signum
negate = Negate
fromInteger a = Const $ fromIntegral a
instance Fractional Expr where
recip = Inverse
fromRational a = Const $ realToFrac a
(/) = Div
instance Floating Expr where
pi = Const pi
exp = Exp
log = Log
sin = Sin
atanh = Atanh
sinh = Sinh
cosh = Cosh
acosh = Acosh
cos = Cos
tan = Tan
asin = Asin
acos = Acos
atan = Atan
asinh = Asinh
fromFunction f = f X
toFunction :: Expr -> (Double -> Double)
toFunction X = \x -> x
toFunction (Negate a) = \a -> (negate a)
toFunction (Const a) = const a
toFunction (Plus a b) = \x -> (toFunction a x) + (toFunction b x)
toFunction (Subtract a b) = \x -> (toFunction a x) - (toFunction b x)
toFunction (Mult a b) = \x -> (toFunction a x) * (toFunction b x)
toFunction (Div a b) = \x -> (toFunction a x) / (toFunction b x)
with_function func x = toFunction $ func $ fromFunction x
inverse X = X
inverse (Const a) = Const a
inverse (Plus (Const a) b) = (Subtract (inverse b) (Const a))
inverse (Plus b (Const a)) = inverse (Plus (Const a) b)
inverse (Mult (Const a) b) = (Div (inverse b) (Const a))
inverse (Mult b (Const a)) = inverse (Mult (Const a) b)
inverse (Negate a) = Negate $ inverse a
inverse (Asin x) = Sin $ inverse x
inverse (Acos x) = Cos $ inverse x
inverse (Atan x) = Tan $ inverse x
inverse_function x = with_function inverse x
Cet exemple ne fonctionne qu'avec des expressions arithmétiques, mais il pourrait probablement être généralisé pour fonctionner également avec des listes.