Nei linguaggi funzionali puri, esiste un algoritmo per ottenere la funzione inversa?


100

In linguaggi funzionali puri come Haskell, esiste un algoritmo per ottenere l'inverso di una funzione, (modifica) quando è biiettiva? E c'è un modo specifico per programmare la tua funzione così è?


5
Matematicamente non è sbagliato dire che, nel caso di f x = 1, l'inverso di 1 è un insieme di numeri interi e l'inverso di qualsiasi altra cosa è un insieme vuoto. Indipendentemente da ciò che dicono alcune risposte, la funzione non essere biiettiva non è il problema più grande.
Karolis Juodelė

2
La risposta corretta è SI, ma non è efficiente. Siano f: A -> B e A finito, quindi, dato b € B, devi "solo" ispezionare tutto f (A) per trovare tutti a € A che f (a) = b. In un computer quantistico, forse avrebbe complessità O (dimensione (a)). Certo, cerchi un algoritmo pratico. Non è (ha O (2 ^ taglia (a))), ma esiste ...
josejuan

QuickCheck lo sta facendo esattamente (cercano un False in f: A -> Bool).
josejuan

4
@ KarolisJuodelė: non sono d'accordo; di solito non è ciò che si intende per inverso. Quasi ogni volta che incontro il termine, l'inverso di fè una funzione gtale che f . g = ide g . f = id. Il tuo candidato non esegue nemmeno il controllo del battito in quel caso.
Ben Millwood

3
@ BenMillwood, hai ragione. Ciò che ho detto si chiama immagine inversa, non funzione inversa. Il mio punto era che le risposte che sottolineano che f x = 1non ha inverso adottano un approccio molto ristretto e ignorano l'intera complessità del problema.
Karolis Juodelė

Risposte:


101

In alcuni casi sì! C'è un bellissimo documento chiamato Bidirectionalization for Free! che discute alcuni casi - quando la tua funzione è sufficientemente polimorfa - dove è possibile, in modo completamente automatico, derivare una funzione inversa. (Discute anche di cosa rende difficile il problema quando le funzioni non sono polimorfiche.)

Quello che ottieni nel caso in cui la tua funzione sia invertibile è l'inverso (con un input spurio); in altri casi, si ottiene una funzione che cerca di "unire" un vecchio valore di input e un nuovo valore di output.


3
Ecco un documento più recente che esamina lo stato dell'arte nella bidirezionalizzazione. Include tre famiglie di tecniche, inclusi approcci "sintattici" e combinatori: iai.uni-bonn.de/~jv/ssgip-bidirectional-final.pdf
sclv

E solo per menzionare, nel 2008 c'era questo messaggio a -cafe, con un malvagio hack per invertire le putfunzioni in qualsiasi struttura di record derivante Data: haskell.org/pipermail/haskell-cafe/2008-April/042193.html usando un approccio simile a quello successivamente presentato (più rigorosamente, più in generale, più basato sui principi, ecc.) in "gratis".
sclv

È il 2017 e ovviamente il link al paper non è più valido ecco quello aggiornato: pdfs.semanticscholar.org/5f0d/…
Mina Gabriel

37

No, in generale non è possibile.

Dimostrazione: considera funzioni biiettive di tipo

type F = [Bit] -> [Bit]

con

data Bit = B0 | B1

Supponiamo di avere un inverter del inv :: F -> Fgenere inv f . f ≡ id. Supponiamo di averlo testato per la funzione f = id, confermandolo

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

Poiché questo primo B0nell'output deve essere arrivato dopo un tempo finito, abbiamo un limite superiore nsia sulla profondità alla quale invaveva effettivamente valutato il nostro input di test per ottenere questo risultato, sia sul numero di volte che può essere chiamato f. Definisci ora una famiglia di funzioni

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

Chiaramente, per tutti 0<j≤n, g jè una biiezione, anzi auto-inversa. Quindi dovremmo essere in grado di confermare

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

ma per adempiere a questo, inv (g j)sarebbe stato necessario entrambi

  • valutare g j (B1 : repeat B0)fino a una profondità din+j > n
  • valutare head $ g j lalmeno ndiversi elenchi di corrispondenzareplicate (n+j) B0 ++ B1 : ls

Fino a quel momento, almeno una delle g jè indistinguibile da f, e poiché inv fnon aveva fatto nessuna di queste valutazioni, invnon avrebbe potuto distinguerla - a meno di fare alcune misurazioni di runtime da solo, il che è possibile solo nel IO Monad.

                                                                                                                                   ⬜


19

Puoi cercarlo su wikipedia, si chiama Reversible Computing .

In generale, però, non puoi farlo e nessuno dei linguaggi funzionali ha questa opzione. Per esempio:

f :: a -> Int
f _ = 1

Questa funzione non ha un inverso.


1
Sarebbe sbagliato dire che fha un inverso, è solo che l'inverso è una funzione non deterministica?
Matt Fenwick

10
@MattFenwick In linguaggi come Haskell, ad esempio, le funzioni semplicemente non sono deterministiche (senza cambiare i tipi e il modo in cui le usi). Non esiste alcuna funzione Haskell g :: Int -> ache sia l'inverso di f, anche se puoi descrivere l'inverso di fmatematicamente.
Ben

2
@ Matt: cerca "in basso" nella programmazione funzionale e nella logica. Un "fondo" è un valore "impossibile", o perché è contraddittorio, senza fine, o perché è la soluzione a un problema indecidibile (questo è più che semplicemente contraddittorio: possiamo "inseguire" metodicamente una soluzione mentre esploriamo un progetto spazio utilizzando "undefined" e "error" durante lo sviluppo). Una x "inferiore" ha il tipo a. "Abita" (o è un "valore") di ogni tipo. Questa è una contraddizione logica, poiché i tipi sono proposizioni e non vi è alcun valore che soddisfi ogni proposizione. Cerca Haskell-Cafe per buone discussioni
nomen

2
@ Matt: Piuttosto che caratterizzare l'inesistenza di inversi in termini di non determinismo, è necessario caratterizzarla in termini di fondi. L'inverso di f _ = 1 è in basso, poiché deve abitare ogni tipo (in alternativa, è in basso poiché f non ha funzione inversa per qualsiasi tipo con più di un singolo elemento - l'aspetto su cui ti sei concentrato, penso). Essere ultimi può essere considerato sia positivamente che negativamente come affermazioni sui valori. Si può ragionevolmente parlare dell'inverso di una funzione arbitraria come se fosse il "valore" inferiore. (Anche se non è "realmente" un valore)
nomen

1
Essendo tornato qui molto più tardi, penso di capire cosa sta arrivando Matt: spesso modelliamo il non determinismo tramite elenchi, e potremmo fare lo stesso per gli inversi. Sia l'inverso di f x = 2 * xsia f' x = [x / 2], e quindi l'inverso di f _ = 1sia f' 1 = [minBound ..]; f' _ = []. Cioè, ci sono molti inversi per 1 e nessuno per qualsiasi altro valore.
amalloy

16

Non nella maggior parte dei linguaggi funzionali, ma nella programmazione logica o relazionale, la maggior parte delle funzioni definite in realtà non sono funzioni ma "relazioni", e queste possono essere utilizzate in entrambe le direzioni. Vedi ad esempio prolog o kanren.


1
O Mercury , che altrimenti condivide molto lo spirito di Haskell. - Buon punto, +1.
sinistra intorno al

11

Compiti come questo sono quasi sempre indecidibili. Puoi avere una soluzione per alcune funzioni specifiche, ma non in generale.

Qui, non puoi nemmeno riconoscere quali funzioni hanno un inverso. Citando Barendregt, HP The Lambda Calculus: Its Syntax and Semantics. Olanda settentrionale, Amsterdam (1984) :

Un insieme di termini lambda non è banale se non è né l'insieme vuoto né l'insieme completo. Se A e B sono due insiemi non banali e disgiunti di termini lambda chiusi sotto l'uguaglianza (beta), allora A e B sono ricorsivamente inseparabili.

Prendiamo A come l'insieme di termini lambda che rappresentano funzioni invertibili e B il resto. Entrambi sono non vuoti e chiusi con l'uguaglianza beta. Quindi non è possibile decidere se una funzione è invertibile o meno.

(Questo vale per il lambda calcolo non tipizzato. TBH Non so se l'argomento può essere adattato direttamente a un lambda calcolo tipizzato quando conosciamo il tipo di una funzione che vogliamo invertire. Ma sono abbastanza sicuro che lo sarà simile.)


11

Se puoi enumerare il dominio della funzione e puoi confrontare gli elementi dell'intervallo per l'uguaglianza, puoi farlo in un modo piuttosto semplice. Con enumerare intendo avere un elenco di tutti gli elementi disponibili. Mi atterrò a Haskell, dal momento che non conosco Ocaml (o anche come capitalizzarlo correttamente ;-)

Quello che vuoi fare è scorrere gli elementi del dominio e vedere se sono uguali all'elemento dell'intervallo che stai cercando di invertire, e prendere il primo che funziona:

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

Dato che hai affermato che fè una biiezione, deve esserci uno e solo uno di questi elementi. Il trucco, ovviamente, è garantire che la tua enumerazione del dominio raggiunga effettivamente tutti gli elementi in un tempo finito . Se stai cercando di invertire una biiezione da Integera Integer, l'utilizzo [0,1 ..] ++ [-1,-2 ..]non funzionerà poiché non arriverai mai ai numeri negativi. In concreto, inv ([0,1 ..] ++ [-1,-2 ..]) (+1) (-3)non produrrà mai un valore.

Tuttavia, 0 : concatMap (\x -> [x,-x]) [1..]funzionerà, poiché questo scorre attraverso gli interi nel seguente ordine [0,1,-1,2,-2,3,-3, and so on]. Anzi inv (0 : concatMap (\x -> [x,-x]) [1..]) (+1) (-3)prontamente ritorna -4!

Il pacchetto Control.Monad.Omega può aiutarti a scorrere gli elenchi di tuple e così via in un buon modo; Sono sicuro che ci sono più pacchetti del genere, ma non li conosco.


Naturalmente, questo approccio è piuttosto semplice e bruta, per non parlare di brutto e inefficiente! Quindi concludo con alcune osservazioni sull'ultima parte della tua domanda, su come "scrivere" le biiezioni. Il sistema di tipi di Haskell non è in grado di dimostrare che una funzione è una biiezione - vuoi davvero qualcosa come Agda per questo - ma è disposto a fidarsi di te.

(Attenzione: segue codice non testato)

Quindi puoi definire un tipo di dati di Bijections tra i tipi ae b:

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

insieme a tutte le costanti (dove puoi dire " So che sono biiezioni!") quante ne desideri, ad esempio:

notBi :: Bi Bool Bool
notBi = Bi not not

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

e un paio di combinatori intelligenti, come:

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)

Penso che potresti fare invert (mapBi add1Bi) [1,5,6]e ottenere [0,4,5]. Se scegli i tuoi combinatori in modo intelligente, penso che il numero di volte che dovrai scrivere una Bicostante a mano potrebbe essere piuttosto limitato.

Dopotutto, se sai che una funzione è una biiezione, si spera che tu abbia un abbozzo di prova di quel fatto nella tua testa, che l'isomorfismo di Curry-Howard dovrebbe essere in grado di trasformare in un programma :-)


6

Recentemente ho affrontato problemi come questo e no, direi che (a) non è difficile in molti casi, ma (b) non è affatto efficiente.

Fondamentalmente, supponi di averlo fatto f :: a -> b, e questa fè davvero una bjiection. Puoi calcolare l'inverso f' :: b -> ain un modo davvero stupido:

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 fè una biiezione e enumerateproduce veramente tutti i valori di a, allora alla fine colpirai un atale che f a == b.

I tipi che hanno una Boundeded Enumun'istanza può essere banalmente fatte RecursivelyEnumerable. Si Enumerablepossono anche creare coppie di tipi 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 stesso vale per le disgiunzioni di Enumerabletipi:

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

Il fatto che possiamo farlo sia per (,)che Eitherprobabilmente significa che possiamo farlo per qualsiasi tipo di dato algebrico.


5

Non tutte le funzioni hanno un inverso. Se si limita la discussione alle funzioni uno a uno, la capacità di invertire una funzione arbitraria garantisce la capacità di craccare qualsiasi sistema crittografico. Dobbiamo sperare che questo non sia fattibile, anche in teoria!


13
Qualsiasi sistema crittografico (esclusi alcuni strani, come un time pad, che non sono fattibili per altri motivi) può essere violato dalla forza bruta. Ciò non li rende meno utili e nessuno dei due sarebbe una funzione di inversione poco pratica.

Davvero? se pensi a una funzione di crittografia come String encrypt(String key, String text)senza la chiave non sarai ancora in grado di fare nulla. EDIT: più quello che ha detto delnan.
mck

@MaciekAlbin Dipende dal tuo modello di attacco. Gli attacchi di testo in chiaro scelti, ad esempio, possono consentire l'estrazione della chiave, che consentirebbe quindi di attaccare altri testi cifrati crittografati con quella chiave.

Con "fattibile" intendevo qualcosa che può essere fatto in un ragionevole lasso di tempo. Non intendevo "computabile" (ne sono abbastanza sicuro).
Jeffrey Scofield

@ JeffreyScofield Capisco il tuo punto. Ma devo dire che sono confuso da "fattibile in teoria" - (la nostra definizione di) fattibilità non si riferisce solo a quanto sia difficile da fare praticamente?

5

In alcuni casi, è possibile trovare l'inverso di una funzione biiettiva convertendola in una rappresentazione simbolica. Sulla base di questo esempio , ho scritto questo programma Haskell per trovare gli inversi di alcune semplici funzioni polinomiali:

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

Questo esempio funziona solo con espressioni aritmetiche, ma probabilmente potrebbe essere generalizzato per lavorare anche con gli elenchi.


4

No, non tutte le funzioni hanno nemmeno gli inversi. Ad esempio, quale sarebbe l'inverso di questa funzione?

f x = 1

La tua funzione è una costante, qui si tratta di funzioni biiettive.
Soleil - Mathieu Prévot
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.