En los lenguajes funcionales puros, ¿existe un algoritmo para obtener la función inversa?

100

En lenguajes funcionales puros como Haskell, ¿existe un algoritmo para obtener la inversa de una función, (editar) cuando es biyectiva? ¿Y hay una forma específica de programar su función para que sea así?

MaiaVictor
fuente
5
Matemáticamente, no es incorrecto decir que, en el caso de f x = 1, el inverso de 1 es un conjunto de números enteros y el inverso de cualquier otra cosa es un conjunto vacío. Independientemente de lo que digan algunas respuestas, la función no ser biyectiva no es el mayor problema.
Karolis Juodelė
2
La respuesta correcta es SÍ, pero no es eficiente. Sea f: A -> B y A finito, entonces, dado b € B, "sólo" debe inspeccionar todo f (A) para encontrar todo a € A que f (a) = b. En una computadora cuántica, quizás tendría O (tamaño (a)) complejidad. Por supuesto, busca un algoritmo práctico. No es (tiene O (2 ^ tamaño (a))), pero existe ...
josejuan
QuickCheck lo está haciendo exactamente (buscan un Falso en f: A -> Bool).
josejuan
4
@ KarolisJuodelė: No estoy de acuerdo; eso no suele ser lo que se entiende por inversa. Casi cada vez que encuentro el término, la inversa de fes una función gtal que f . g = idy g . f = id. En ese caso, su candidato ni siquiera realiza la verificación a máquina.
Ben Millwood
3
@BenMillwood, tienes razón. Lo que dije se llama imagen inversa, no función inversa. Mi punto fue que las respuestas señalando que f x = 1no tiene inversa adoptan un enfoque muy estrecho e ignoran toda la complejidad del problema.
Karolis Juodelė

Respuestas:

101

En algunos casos, ¡sí! ¡Hay un hermoso artículo llamado Bidireccionalización gratis! que analiza algunos casos, cuando su función es suficientemente polimórfica, donde es posible, de forma completamente automática, derivar una función inversa. (También analiza qué dificulta el problema cuando las funciones no son polimórficas).

Lo que se obtiene en el caso de que su función sea invertible es la inversa (con una entrada falsa); en otros casos, obtiene una función que intenta "fusionar" un valor de entrada antiguo y un valor de salida nuevo.

Daniel Wagner
fuente
3
Aquí hay un artículo más reciente que analiza el estado del arte en bidireccionalización. Incluye tres familias de técnicas, incluidos los enfoques "sintácticos" y basados ​​en combinadores: iai.uni-bonn.de/~jv/ssgip-bidirectional-final.pdf
sclv
Y solo para mencionar, en 2008 hubo este mensaje para -cafe, con un truco maligno para invertir putfunciones en cualquier estructura de registro que derive Data: haskell.org/pipermail/haskell-cafe/2008-April/042193.html usando un enfoque similar a que luego presentó (más rigurosamente, más en general, más principios, etc.) en "gratis".
sclv
Es 2017 y, por supuesto, el enlace al artículo ya no es válido, aquí está el actualizado: pdfs.semanticscholar.org/5f0d/…
Mina Gabriel
37

No, no es posible en general.

Prueba: considere funciones biyectivas de tipo

type F = [Bit] -> [Bit]

con

data Bit = B0 | B1

Supongamos que tenemos un inversor inv :: F -> Ftal que inv f . f ≡ id. Digamos que lo hemos probado para la función f = id, confirmando que

inv f (repeat B0) -> (B0 : ls)

Dado que este primero B0en la salida debe haber llegado después de un tiempo finito, tenemos un límite superior ntanto en la profundidad a la que invrealmente se evaluó nuestra entrada de prueba para obtener este resultado, como en el número de veces que puede haber llamado f. Defina ahora una familia de funciones

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

Claramente, para todos 0<j≤n, g jes una biyección, de hecho, autoinversa. Entonces deberíamos poder confirmar

inv (g j) (replicate (n+j) B0 ++ B1 : repeat B0) -> (B1 : ls)

pero para cumplir con esto, inv (g j)habría necesitado

  • evaluar g j (B1 : repeat B0)a una profundidad den+j > n
  • evaluar head $ g j lal menos ndiferentes listas que coincidanreplicate (n+j) B0 ++ B1 : ls

Hasta ese momento, al menos uno de los g jes indistinguible de f, y dado que inv fno había realizado ninguna de estas evaluaciones, invno podría haberlo diferenciado, salvo realizar algunas mediciones en tiempo de ejecución por sí solo, lo que solo es posible en el IO Monad.

                                                                                                                                   ⬜

izquierda
fuente
19

Puede buscarlo en wikipedia, se llama Computación reversible .

Sin embargo, en general no puede hacerlo y ninguno de los lenguajes funcionales tiene esa opción. Por ejemplo:

f :: a -> Int
f _ = 1

Esta función no tiene inversa.

mck
fuente
1
¿Sería incorrecto decir que ftiene una inversa, es solo que la inversa es una función no determinista?
Matt Fenwick
10
@MattFenwick En lenguajes como Haskell, por ejemplo, las funciones simplemente no son no deterministas (sin cambiar los tipos y la forma en que las usa). No existe ninguna función de Haskell g :: Int -> aque sea la inversa de f, incluso si puede describir la inversa de fmatemáticamente.
Ben
2
@Matt: Busque "abajo" en programación funcional y lógica. Un "fondo" es un valor "imposible", ya sea porque es contradictorio, no final o la solución a un problema indecidible (esto es más que simplemente contradictorio: podemos "perseguir" metódicamente una solución mientras exploramos un diseño espacio usando "indefinido" y "error" durante el desarrollo). Un "fondo" x tiene el tipo a. "Habita" (o es un "valor") de todo tipo. Ésta es una contradicción lógica, ya que los tipos son proposiciones y no hay valor que satisfaga todas las proposiciones. Busque en Haskell-Cafe para obtener buenas discusiones
nomen
2
@Matt: Más que caracterizar la inexistencia de inversas en términos de no determinismo, hay que caracterizarla en términos de fondos. La inversa de f _ = 1 es inferior, ya que debe habitar todos los tipos (alternativamente, es inferior, ya que f no tiene función inversa para ningún tipo con más de un elemento, creo que el aspecto en el que se centró). Estar en el fondo puede tomarse tanto positiva como negativamente como afirmaciones sobre valores. Se puede hablar con sensatez de la inversa de una función arbitraria como el fondo del "valor". (Aunque no es "realmente" un valor)
nomen
1
Habiendo regresado aquí mucho más tarde, creo que veo a lo que Matt quiere llegar: a menudo modelamos el no determinismo a través de listas, y podríamos hacer lo mismo con las inversas. Sea el inverso de f x = 2 * xser f' x = [x / 2], y luego el inverso de f _ = 1es f' 1 = [minBound ..]; f' _ = []. Es decir, hay muchas inversas para 1 y ninguna para cualquier otro valor.
amalloy
16

No en la mayoría de los lenguajes funcionales, pero en la programación lógica o la programación relacional, la mayoría de las funciones que define no son funciones, sino "relaciones", y se pueden utilizar en ambas direcciones. Consulte, por ejemplo, prolog o kanren.

amalloy
fuente
1
O Mercury , que por lo demás comparte mucho del espíritu de Haskell. - Buen punto, +1.
izquierda rotonda sobre el
11

Tareas como esta casi siempre son indecidibles. Puede tener una solución para algunas funciones específicas, pero no en general.

Aquí, ni siquiera puede reconocer qué funciones tienen una inversa. Citando a Barendregt, HP El cálculo Lambda: su sintaxis y semántica. Holanda Septentrional, Amsterdam (1984) :

Un conjunto de términos lambda no es trivial si no es ni el conjunto vacío ni el completo. Si A y B son dos conjuntos no triviales, disjuntos de términos lambda cerrados bajo igualdad (beta), entonces A y B son recursivamente inseparables.

Tomemos A como el conjunto de términos lambda que representan funciones invertibles y B el resto. Ambos no están vacíos y cerrados bajo igualdad beta. Por tanto, no es posible decidir si una función es invertible o no.

(Esto se aplica al cálculo lambda sin tipo. TBH no sé si el argumento se puede adaptar directamente a un cálculo lambda con tipo cuando conocemos el tipo de función que queremos invertir. Pero estoy bastante seguro de que será similar.)

Petr Pudlák
fuente
11

Si puede enumerar el dominio de la función y comparar elementos del rango para determinar la igualdad, puede hacerlo de una manera bastante sencilla. Por enumerar me refiero a tener una lista de todos los elementos disponibles. Me quedaré con Haskell, ya que no sé Ocaml (ni siquiera cómo capitalizarlo correctamente ;-)

Lo que desea hacer es ejecutar los elementos del dominio y ver si son iguales al elemento del rango que está tratando de invertir, y tomar el primero que funcione:

inv :: Eq b => [a] -> (a -> b) -> (b -> a)
inv domain f b = head [ a | a <- domain, f a == b ]

Como ha dicho que fes una biyección, es probable que haya uno y solo uno de esos elementos. El truco, por supuesto, es asegurarse de que su enumeración del dominio alcance realmente todos los elementos en un tiempo finito . Si está tratando de invertir una biyección de Integera Integer, el uso [0,1 ..] ++ [-1,-2 ..]no funcionará ya que nunca obtendrá los números negativos. Concretamente,inv ([0,1 ..] ++ [-1,-2 ..]) (+1) (-3) nunca rendirá un valor.

Sin embargo, 0 : concatMap (\x -> [x,-x]) [1..]funcionará, ya que se ejecuta a través de los números enteros en el siguiente orden [0,1,-1,2,-2,3,-3, and so on]. De hecho inv (0 : concatMap (\x -> [x,-x]) [1..]) (+1) (-3)regresa rápidamente-4 !

El paquete Control.Monad.Omega puede ayudarlo a ejecutar listas de tuplas, etcétera, de una buena manera; Estoy seguro de que hay más paquetes como ese, pero no los conozco.


Por supuesto, este enfoque es bastante sencillo y de fuerza bruta, ¡sin mencionar que es feo e ineficiente! Así que terminaré con algunos comentarios sobre la última parte de su pregunta, sobre cómo 'escribir' bijecciones. El sistema de tipos de Haskell no está preparado para demostrar que una función es una biyección (realmente quieres algo como Agda para eso) pero está dispuesto a confiar en ti.

(Advertencia: sigue un código no probado)

Entonces, ¿puede definir un tipo de datos de Bijections entre tipos ay b:

data Bi a b = Bi {
    apply :: a -> b,
    invert :: b -> a 
}

junto con tantas constantes (donde puede decir '¡ que son biyecciones!') como desee, como:

notBi :: Bi Bool Bool
notBi = Bi not not

add1Bi :: Bi Integer Integer
add1Bi = Bi (+1) (subtract 1)

y un par de combinadores inteligentes, como:

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)

Creo que entonces podrías hacer invert (mapBi add1Bi) [1,5,6]y conseguir [0,4,5]. Si elige sus combinadores de una manera inteligente, creo que la cantidad de veces que tendrá que escribir unBi constante a mano podría ser bastante limitada.

Después de todo, si sabe que una función es una biyección, es de esperar que tenga un bosquejo de prueba de ese hecho en su cabeza, que el isomorfismo de Curry-Howard debería poder convertir en un programa :-)

yatima2975
fuente
6

Recientemente he estado lidiando con problemas como este, y no, diría que (a) no es difícil en muchos casos, pero (b) no es eficiente en absoluto.

Básicamente, suponga que lo ha hecho f :: a -> b, y eso fes de hecho un error. Puedes calcular la inversa f' :: b -> ade una manera realmente tonta:

import Data.List

-- | Class for types whose values are recursively enumerable.
class Enumerable a where
    -- | Produce the list of all values of type @a@.
    enumerate :: [a]

 -- | Note, this is only guaranteed to terminate if @f@ is a bijection!
invert :: (Enumerable a, Eq b) => (a -> b) -> b -> Maybe a
invert f b = find (\a -> f a == b) enumerate

Si fes una biyección y enumeraterealmente produce todos los valores de a, eventualmente llegará a atal que f a == b.

Los tipos que tienen una instancia Boundedy una se Enumpueden hacer trivialmente RecursivelyEnumerable. EnumerableTambién se pueden hacer pares de tipos Enumerable:

instance (Enumerable a, Enumerable b) => Enumerable (a, b) where
    enumerate = crossWith (,) enumerate enumerate

crossWith :: (a -> b -> c) -> [a] -> [b] -> [c]
crossWith f _ [] = []
crossWith f [] _ = []
crossWith f (x0:xs) (y0:ys) =
    f x0 y0 : interleave (map (f x0) ys) 
                         (interleave (map (flip f y0) xs)
                                     (crossWith f xs ys))

interleave :: [a] -> [a] -> [a]
interleave xs [] = xs
interleave [] ys = []
interleave (x:xs) ys = x : interleave ys xs

Lo mismo ocurre con las disyunciones de Enumerabletipos:

instance (Enumerable a, Enumerable b) => Enumerable (Either a b) where
    enumerate = enumerateEither enumerate enumerate

enumerateEither :: [a] -> [b] -> [Either a b]
enumerateEither [] ys = map Right ys
enumerateEither xs [] = map Left xs
enumerateEither (x:xs) (y:ys) = Left x : Right y : enumerateEither xs ys

El hecho de que podamos hacer esto para (,)y Eitherprobablemente significa que podemos hacerlo para cualquier tipo de datos algebraicos.

Luis Casillas
fuente
5

No todas las funciones tienen una inversa. Si limita la discusión a funciones uno a uno, la capacidad de invertir una función arbitraria otorga la capacidad de descifrar cualquier criptosistema. Tenemos que esperar que esto no sea factible, ¡incluso en teoría!

Jeffrey Scofield
fuente
13
Cualquier criptosistema (excluyendo algunos impares, como los pads únicos, que no son factibles por otras razones) puede ser roto por la fuerza bruta. Eso no los hace menos útiles, y tampoco sería una función de inversión imprácticamente cara.
¿De verdad? si piensa en una función de cifrado String encrypt(String key, String text)sin la clave, todavía no podrá hacer nada. EDITAR: Además de lo que dijo delnan.
mck
@MaciekAlbin Depende de su modelo de ataque. Los ataques de texto sin formato elegidos, por ejemplo, pueden permitir extraer la clave, lo que luego permitiría atacar otros textos cifrados encriptados con esa clave.
Por "factible" me refiero a algo que se puede hacer en un tiempo razonable. No quise decir "computable" (estoy bastante seguro).
Jeffrey Scofield
@JeffreyScofield Veo tu punto. Pero tengo que decir que estoy confundido con "factible en teoría" - ¿no (nuestra definición de) factibilidad solo se refiere a lo difícil que es hacerlo prácticamente?
5

En algunos casos, es posible encontrar la inversa de una función biyectiva convirtiéndola en una representación simbólica. Basado en este ejemplo , escribí este programa de Haskell para encontrar inversas de algunas funciones polinomiales simples:

bijective_function x = x*2+1

main = do
    print $ bijective_function 3
    print $ inverse_function bijective_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

simplify X = X
simplify (Div (Const a) (Const b)) = Const (a/b)
simplify (Mult (Const a) (Const b)) | a == 0 || b == 0 = 0 | otherwise = Const (a*b)
simplify (Negate (Negate a)) = simplify a
simplify (Subtract a b) = simplify ( Plus (simplify a) (Negate (simplify b)) )
simplify (Div a b) | a == b = Const 1.0 | otherwise = simplify (Div (simplify a) (simplify b))
simplify (Mult a b) = simplify (Mult (simplify a) (simplify b))
simplify (Const a) = Const a
simplify (Plus (Const a) (Const b)) = Const (a+b)
simplify (Plus a (Const b)) = simplify (Plus (Const b) (simplify a))
simplify (Plus (Mult (Const a) X) (Mult (Const b) X)) = (simplify (Mult (Const (a+b)) X))
simplify (Plus (Const a) b) = simplify (Plus (simplify b) (Const a))
simplify (Plus X a) = simplify (Plus (Mult 1 X) (simplify a))
simplify (Plus a X) = simplify (Plus (Mult 1 X) (simplify a))
simplify (Plus a b) = (simplify (Plus (simplify a) (simplify b)))
simplify a = a

inverse X = X
inverse (Const a) = simplify (Const a)
inverse (Mult (Const a) (Const b)) = Const (a * b)
inverse (Mult (Const a) X) = (Div X (Const a))
inverse (Plus X (Const a)) = (Subtract X (Const a))
inverse (Negate x) = Negate (inverse x)
inverse a = inverse (simplify a)

inverse_function x = with_function inverse x

Este ejemplo solo funciona con expresiones aritméticas, pero probablemente podría generalizarse para trabajar también con listas.

Anderson Green
fuente
4

No, ni siquiera todas las funciones tienen inversas. Por ejemplo, ¿cuál sería la inversa de esta función?

f x = 1
Dirk Holsopple
fuente
Tu función es una constante, aquí se trata de funciones biyectivas.
Soleil - Mathieu Prévot