Em linguagens puramente funcionais, existe um algoritmo para obter a função inversa?

100

Em linguagens puramente funcionais como Haskell, existe um algoritmo para obter o inverso de uma função, (editar) quando ela é bijetiva? E existe uma maneira específica de programar sua função assim?

MaiaVictor
fonte
5
Matematicamente, não é errado dizer que, no caso de f x = 1, o inverso de 1 é um conjunto de inteiros e o inverso de qualquer outra coisa é um conjunto vazio. Independentemente do que digam algumas respostas, a função não ser bijetiva não é o maior problema.
Karolis Juodelė
2
A resposta correta é SIM, mas não é eficiente. Se f: A -> B e A finito, então, dado b € B, você "apenas" deve inspecionar todos os f (A) para encontrar todos os a € A que f (a) = b. Em um computador quântico, talvez tivesse complexidade de O (tamanho (a)). Claro, você procura um algoritmo prático. Não é (tem O (2 ^ tamanho (a))), mas existe ...
josejuan
QuickCheck está fazendo exatamente isso (eles procuram um False em f: A -> Bool).
Josejuan
4
@ KarolisJuodelė: Eu discordo; geralmente não é isso o que significa inverso. Quase sempre que encontro o termo, o inverso de fé uma função gque f . g = ide g . f = id. Seu candidato nem mesmo verifica nesse caso.
Ben Millwood
3
@BenMillwood, você está certo. O que eu disse é chamado de imagem inversa, não função inversa. Meu ponto é que as respostas que apontam que f x = 1não tem o inverso têm uma abordagem muito estreita e ignoram toda a complexidade do problema.
Karolis Juodelė

Respostas:

101

Em alguns casos, sim! Há um belo artigo chamado Bidirecionalização de graça! que discute alguns casos - quando sua função é suficientemente polimórfica - onde é possível, de forma totalmente automática, derivar uma função inversa. (Também discute o que torna o problema difícil quando as funções não são polimórficas.)

O que você obtém no caso de sua função ser invertível é o inverso (com uma entrada espúria); em outros casos, você obtém uma função que tenta "mesclar" um valor de entrada antigo e um novo valor de saída.

Daniel Wagner
fonte
3
Aqui está um artigo mais recente que pesquisa o estado da arte em bidirecionalização. Inclui três famílias de técnicas, incluindo abordagens "sintáticas" e combinadoras: iai.uni-bonn.de/~jv/ssgip-bidirectional-final.pdf
sclv
E só para mencionar, em 2008, houve esta mensagem para -cafe, com um hack maligno para inverter putfunções em qualquer estrutura de registro derivando Data: haskell.org/pipermail/haskell-cafe/2008-April/042193.html usando uma abordagem semelhante a que posteriormente apresentado (de forma mais rigorosa, mais geral, mais baseada em princípios, etc.) em "de graça".
sclv
Estamos em 2017 e, claro, o link para o artigo não é mais válido aqui está o atualizado: pdfs.semanticscholar.org/5f0d/…
Mina Gabriel
37

Não, geralmente não é possível.

Prova: considere funções bijetivas do tipo

type F = [Bit] -> [Bit]

com

data Bit = B0 | B1

Suponha que temos um inversor inv :: F -> Fdesse tipo inv f . f ≡ id. Digamos que o tenhamos testado para a função f = id, confirmando que

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

Como esse primeiro B0na saída deve ter ocorrido depois de algum tempo finito, temos um limite superior nna profundidade para a qual invrealmente avaliamos nossa entrada de teste para obter esse resultado, bem como o número de vezes que ela pode ter sido chamada f. Defina agora uma família de funções

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 jé uma bijeção, na verdade autoinversa. Portanto, devemos ser capazes de confirmar

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

mas para cumprir isso, inv (g j)teria que

  • avaliar g j (B1 : repeat B0)a uma profundidade den+j > n
  • avaliar, head $ g j lpelo menos, nlistas diferentes correspondentesreplicate (n+j) B0 ++ B1 : ls

Até aquele ponto, pelo menos um dos g jé indistinguível de f, e uma vez inv fque não tinha feito nenhuma dessas avaliações, invnão poderia tê-lo diferenciado - além de fazer algumas medições de tempo de execução por conta própria, o que só é possível no IO Monad.

                                                                                                                                   ⬜

à esquerda
fonte
19

Você pode procurar na wikipedia, é chamado de Computação Reversível .

Em geral, você não pode fazer isso e nenhuma das linguagens funcionais tem essa opção. Por exemplo:

f :: a -> Int
f _ = 1

Esta função não possui inverso.

mck
fonte
1
Seria errado dizer que ftem um inverso, mas o inverso é uma função não determinística?
Matt Fenwick de
10
@MattFenwick Em linguagens como Haskell, por exemplo, as funções simplesmente não são não-determinísticas (sem alterar os tipos e a maneira como você os usa). Não existe nenhuma função Haskell g :: Int -> aque seja o inverso de f, mesmo que você possa descrever o inverso de fmatematicamente.
Ben
2
@Matt: Procure "fundo" em programação funcional e lógica. Um "fundo" é um valor "impossível", seja porque é contraditório, não finalizante ou a solução para um problema indecidível (isso é mais do que meramente contraditório - podemos metodicamente "perseguir" uma solução enquanto exploramos um design usando "indefinido" e "erro" durante o desenvolvimento). Um x "inferior" tem o tipo a. Ele "habita" (ou é um "valor") de todo tipo. Esta é uma contradição lógica, uma vez que os tipos são proposições e não há valor que satisfaça todas as proposições. Procure no Haskell-Cafe para boas discussões
nomen
2
@Matt: Em vez de caracterizar a inexistência de inversos em termos de não determinismo, deve-se caracterizá-lo em termos de fundos. O inverso de f _ = 1 é bottom, já que deve habitar todos os tipos (alternativamente, é bottom, já que f não tem função inversa para qualquer tipo com mais de um elemento - o aspecto em que você focou, eu acho). Estar no fundo do poço pode ser considerado positiva ou negativamente como afirmações sobre valores. Pode-se falar sensatamente do inverso de uma função arbitrária como sendo o fundo do "valor". (Mesmo que não seja "realmente" um valor)
nomen
1
Tendo voltado aqui muito mais tarde, acho que entendo aonde Matt está chegando: muitas vezes modelamos o não-determinismo por meio de listas e poderíamos fazer o mesmo para inversos. Deixe o inverso de f x = 2 * xser f' x = [x / 2], e então o inverso de f _ = 1é f' 1 = [minBound ..]; f' _ = []. Ou seja, existem muitos inversos para 1 e nenhum para qualquer outro valor.
Amalloy
16

Não na maioria das linguagens funcionais, mas na programação lógica ou na programação relacional, a maioria das funções que você define não são funções, mas "relações", e podem ser usadas em ambas as direções. Veja, por exemplo, prólogo ou kanren.

amalloy
fonte
1
Ou Mercury , que de outra forma compartilha muito do espírito de Haskell. - Bom ponto, +1.
esquerda por volta de
11

Tarefas como essa quase sempre são indecidíveis. Você pode ter uma solução para algumas funções específicas, mas não em geral.

Aqui, você nem consegue reconhecer quais funções têm um inverso. Citando Barendregt, HP The Lambda Calculus: Its Syntax and Semantics. North Holland, Amsterdam (1984) :

Um conjunto de termos lambda não é trivial se não for o conjunto vazio nem completo. Se A e B são dois conjuntos não triviais e disjuntos de termos lambda fechados sob a igualdade (beta), então A e B são recursivamente inseparáveis.

Vamos considerar A como o conjunto de termos lambda que representam funções invertíveis e B o resto. Ambos são não vazios e fechados sob igualdade beta. Portanto, não é possível decidir se uma função é invertível ou não.

(Isso se aplica ao cálculo lambda não digitado. TBH Não sei se o argumento pode ser adaptado diretamente a um cálculo lambda digitado quando sabemos o tipo de função que queremos inverter. Mas tenho certeza de que será semelhante.)

Petr Pudlák
fonte
11

Se você pode enumerar o domínio da função e pode comparar os elementos do intervalo para igualdade, você pode - de uma maneira bastante direta. Por enumerar, quero dizer ter uma lista de todos os elementos disponíveis. Vou ficar com Haskell, já que não conheço Ocaml (ou mesmo como capitalizá-lo corretamente ;-)

O que você quer fazer é percorrer os elementos do domínio e ver se eles são iguais ao elemento do intervalo que você está tentando inverter e pegar o primeiro que funcione:

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

Já que você declarou que fé uma bijeção, deve haver um e apenas um desses elementos. O truque, é claro, é garantir que sua enumeração do domínio realmente alcance todos os elementos em um tempo finito . Se você está tentando inverter uma bijeção de Integerpara Integer, o uso [0,1 ..] ++ [-1,-2 ..]não funcionará, pois você nunca obterá os números negativos. Concretamente, inv ([0,1 ..] ++ [-1,-2 ..]) (+1) (-3)nunca renderá um valor.

No entanto, 0 : concatMap (\x -> [x,-x]) [1..]funcionará, visto que percorre os números inteiros na seguinte ordem [0,1,-1,2,-2,3,-3, and so on]. Na verdade inv (0 : concatMap (\x -> [x,-x]) [1..]) (+1) (-3)retorna prontamente -4!

O pacote Control.Monad.Omega pode ajudá-lo a percorrer listas de tuplas, etc. de uma boa maneira; Tenho certeza de que há mais pacotes como esse - mas não os conheço.


Claro, essa abordagem é bastante simples e bruta, para não mencionar feia e ineficiente! Portanto, terminarei com algumas observações sobre a última parte da sua pergunta, sobre como 'escrever' bijeções. O sistema de tipos de Haskell não prova que uma função é uma bijeção - você realmente quer algo como Agda para isso - mas ele está disposto a confiar em você.

(Aviso: segue código não testado)

Então, você pode definir um tipo de dados de Bijections entre os tipos ae b:

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

junto com quantas constantes (onde você pode dizer 'Eu sei que são bijetivas!') quanto desejar, como:

notBi :: Bi Bool Bool
notBi = Bi not not

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

e alguns 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)

Acho que você poderia então fazer invert (mapBi add1Bi) [1,5,6]e obter [0,4,5]. Se você escolher seus combinadores de maneira inteligente, acho que o número de vezes que terá que escrever uma Biconstante à mão pode ser bastante limitado.

Afinal, se você sabe que uma função é uma bijeção, você terá um esboço de prova desse fato em sua cabeça, que o isomorfismo de Curry-Howard deve ser capaz de transformar em um programa :-)

yatima2975
fonte
6

Recentemente, tenho lidado com questões como essa e não, eu diria que (a) não é difícil em muitos casos, mas (b) não é nada eficiente.

Basicamente, suponha que você tenha f :: a -> b, e isso fé de fato uma bjiection. Você pode calcular o inverso f' :: b -> ade uma maneira realmente estúpida:

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

Se ffor uma bijeção e enumeraterealmente produzir todos os valores de a, então você acabará atingindo atal que f a == b.

Tipos que possuem ae Boundeduma Enuminstância podem ser feitos trivialmente RecursivelyEnumerable. Pares de Enumerabletipos também podem ser feitos 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

O mesmo vale para disjunções 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

O fato de que podemos fazer isso para (,)e Eitherprovavelmente significa que podemos fazer para qualquer tipo de dados algébrico.

Luis casillas
fonte
5

Nem toda função possui um inverso. Se você limitar a discussão a funções um-para-um, a capacidade de inverter uma função arbitrária garante a capacidade de quebrar qualquer criptosistema. Esperamos que isso não seja viável, mesmo em teoria!

Jeffrey Scofield
fonte
13
Qualquer criptosistema (exceto alguns estranhos, como blocos de tempo, que são inviáveis ​​por outros motivos) pode ser quebrado por força bruta. Isso não os torna menos úteis, e também não seria uma função de inversão impraticamente cara.
Realmente? se você pensar em uma função de criptografia String encrypt(String key, String text)sem a chave, você ainda não será capaz de fazer nada. EDIT: Além do que disse delnan.
março de
@MaciekAlbin Depende do seu modelo de ataque. Ataques de texto simples escolhidos, por exemplo, podem permitir a extração da chave, o que permitiria atacar outros textos cifrados criptografados com essa chave.
Por "viável" eu quis dizer algo que pode ser feito em qualquer período de tempo razoável. Eu não quis dizer "computável" (tenho certeza).
Jeffrey Scofield
@JeffreyScofield Entendo seu ponto. Mas devo dizer que estou confuso com "viável em teoria" - (nossa definição de) viabilidade não se refere apenas a quão difícil é fazer na prática?
5

Em alguns casos, é possível encontrar o inverso de uma função bijetivo convertendo-a em uma representação simbólica. Com base neste exemplo , escrevi este programa Haskell para encontrar inversos de algumas funções polinomiais 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 exemplo funciona apenas com expressões aritméticas, mas provavelmente poderia ser generalizado para funcionar com listas também.

Anderson Green
fonte
4

Não, nem todas as funções têm inversos. Por exemplo, qual seria o inverso dessa função?

f x = 1
Dirk Holsopple
fonte
Sua função é uma constante, aqui se trata de funções bijetivas.
Soleil - Mathieu Prévot