Trong các ngôn ngữ hàm thuần túy, có thuật toán nào để lấy hàm ngược không?


100

Trong các ngôn ngữ hàm thuần túy như Haskell, có thuật toán nào để lấy nghịch đảo của một hàm, (chỉnh sửa) khi nó là bijective không? Và có một cách cụ thể để lập trình chức năng của bạn như vậy không?


5
Về mặt toán học, không sai khi nói rằng, trong trường hợp của f x = 1, nghịch đảo của 1 là một tập hợp các số nguyên và nghịch đảo của bất kỳ thứ gì khác là một tập rỗng. Bất kể một số câu trả lời nói gì, chức năng không mang tính phân tích không phải là vấn đề lớn nhất.
Karolis Juodelė

2
Câu trả lời đúng là CÓ, nhưng nó không hiệu quả. Cho f: A -> B và A hữu hạn, khi đó, với b € B, bạn "chỉ" phải kiểm tra tất cả f (A) để tìm tất cả a € A mà f (a) = b. Trong một máy tính lượng tử, có lẽ sẽ có độ phức tạp O (size (a)). Tất nhiên, bạn tìm kiếm một thuật toán thực tế. Nó không phải là (có O (2 ^ kích thước (a))), nhưng tồn tại ...
josejuan

QuickCheck đang thực hiện chính xác (họ tìm Sai trong f: A -> Bool).
josejuan

4
@ KarolisJuodelė: Tôi không đồng ý; đó thường không phải là ý nghĩa của nghịch đảo. Khá nhiều khi tôi gặp thuật ngữ, nghịch đảo của flà một hàm gnhư vậy f . g = idg . f = id. Ứng viên của bạn thậm chí không đánh máy trong trường hợp đó.
Ben Millwood

3
@BenMillwood, bạn nói đúng. Những gì tôi đã nói được gọi là một hình ảnh nghịch đảo, không phải là một hàm ngược. Quan điểm của tôi là các câu trả lời chỉ ra f x = 1không có nghịch đảo có cách tiếp cận rất hẹp và bỏ qua toàn bộ sự phức tạp của vấn đề.
Karolis Juodelė

Câu trả lời:


101

Trong một số trường hợp, có! Có một bài báo tuyệt đẹp được gọi là Hai chiều miễn phí! trong đó thảo luận về một số trường hợp - khi hàm của bạn đủ đa hình - khi có thể, hoàn toàn tự động để lấy ra một hàm ngược. (Nó cũng thảo luận về điều gì làm cho vấn đề trở nên khó khăn khi các hàm không đa hình.)

Những gì bạn nhận được trong trường hợp hàm của bạn là khả nghịch là nghịch đảo (với đầu vào giả); trong các trường hợp khác, bạn nhận được một hàm cố gắng "hợp nhất" một giá trị đầu vào cũ và một giá trị đầu ra mới.


3
Đây là một bài báo gần đây hơn khảo sát trạng thái của nghệ thuật trong việc phân tích hai chiều. Nó bao gồm ba nhóm kỹ thuật, bao gồm cả các phương pháp tiếp cận dựa trên "cú pháp" và tổ hợp: iai.uni-bonn.de/~jv/ssgip-bidirectional-final.pdf
sclv

Và chỉ cần đề cập, vào năm 2008, có thông báo này tới -cafe, với một cuộc tấn công ác độc để đảo các puthàm vào bất kỳ cấu trúc bản ghi nào dẫn xuất Data: haskell.org/pipermail/haskell-cafe/2008-April/042193.html bằng cách sử dụng cách tiếp cận tương tự như mà sau này được trình bày (chặt chẽ hơn, tổng quát hơn, nguyên tắc hơn, v.v.) trong "miễn phí".
sclv

Đó là năm 2017 và tất nhiên liên kết đến bài báo không còn giá trị nữa, đây là liên kết được cập nhật: pdfs.semanticscholar.org/5f0d/…
Mina Gabriel

37

Không, nói chung là không thể.

Chứng minh: hãy xem xét các hàm lưỡng tính của kiểu

type F = [Bit] -> [Bit]

với

data Bit = B0 | B1

Giả sử chúng ta có một biến tần inv :: F -> Fnhư vậy inv f . f ≡ id. Giả sử chúng tôi đã kiểm tra nó cho chức năng f = id, bằng cách xác nhận rằng

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

Vì điều này đầu tiên B0trong đầu ra phải đến sau một khoảng thời gian hữu hạn, chúng tôi có giới hạn ntrên về cả độ sâu invđã thực sự đánh giá đầu vào thử nghiệm của chúng tôi để có được kết quả này, cũng như số lần nó có thể được gọi f. Xác định bây giờ một nhóm các hàm

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

Rõ ràng, đối với tất cả 0<j≤n, g jlà một sự phủ định, trên thực tế là tự ngược. Vì vậy, chúng tôi sẽ có thể xác nhận

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

nhưng để thực hiện điều này, inv (g j)cần phải

  • đánh giá g j (B1 : repeat B0)đến độ sâu củan+j > n
  • đánh giá head $ g j lcho ít nhất ncác danh sách khác nhau phù hợpreplicate (n+j) B0 ++ B1 : ls

Cho đến thời điểm đó, ít nhất một trong số g jkhông thể phân biệt được f, và vì inv fchưa thực hiện một trong hai đánh giá này, invnên không thể phân biệt được điều đó - thiếu việc thực hiện một số phép đo thời gian chạy, điều này chỉ có thể thực hiện được trong IO Monad.

                                                                                                                                   ⬜


19

Bạn có thể tra cứu nó trên wikipedia, nó có tên là Reversible Computing .

Nói chung, bạn không thể làm điều đó mặc dù và không có ngôn ngữ chức năng nào có tùy chọn đó. Ví dụ:

f :: a -> Int
f _ = 1

Hàm này không có nghịch đảo.


1
Sẽ là sai khi nói rằng fkhông có một nghịch đảo, nó chỉ là nghịch đảo là một hàm không xác định?
Matt Fenwick

10
@MattFenwick Trong một ngôn ngữ như Haskell chẳng hạn, các hàm đơn giản không phải là không xác định (mà không cần thay đổi kiểu và cách bạn sử dụng chúng). Không tồn tại bất kỳ hàm Haskell g :: Int -> anào là nghịch đảo của f, ngay cả khi bạn có thể mô tả nghịch đảo của ftoán học.
Ben

2
@Matt: Tra cứu "bottom" trong lập trình hàm và logic. Giá trị "đáy" là giá trị "không thể", vì nó mâu thuẫn, không kết thúc hoặc là giải pháp cho một vấn đề không thể quyết định (điều này không chỉ đơn thuần là mâu thuẫn - chúng ta có thể "theo đuổi" một giải pháp một cách có phương pháp trong khi chúng ta khám phá một thiết kế không gian sử dụng "không xác định" và "lỗi" trong quá trình phát triển). Dấu x "đáy" có kiểu là a. Nó "sinh sống" (hoặc là một "giá trị") của mọi loại. Đây là một mâu thuẫn logic, vì các loại là mệnh đề và không có giá trị nào thỏa mãn mọi mệnh đề. Hãy xem Haskell-Cafe để có những cuộc thảo luận hay
nomen

2
@Matt: Thay vì mô tả sự không tồn tại của nghịch đảo về mặt không xác định, người ta phải mô tả nó về mặt đáy. Nghịch đảo của f _ = 1 là đáy, vì nó phải tồn tại ở mọi kiểu (cách khác, nó là đáy vì f không có hàm ngược cho bất kỳ kiểu nào có nhiều hơn một phần tử - tôi nghĩ là khía cạnh bạn tập trung vào). Việc nằm dưới cùng có thể được coi là tích cực và tiêu cực như một sự khẳng định về giá trị. Người ta có thể nói một cách hợp lý về nghịch đảo của một hàm tùy ý là giá trị dưới cùng. (Mặc dù nó không phải là một giá trị "thực sự")
nomen,

1
Sau đó nhiều lần lang thang trở lại đây, tôi nghĩ rằng tôi hiểu được những gì Matt đang đạt được: chúng tôi thường mô hình hóa thuyết không xác định thông qua các danh sách và chúng tôi cũng có thể làm điều tương tự đối với các phần nghịch đảo. Giả sử nghịch đảo của f x = 2 * xf' x = [x / 2], và sau đó nghịch đảo của f _ = 1f' 1 = [minBound ..]; f' _ = []. Có nghĩa là, có nhiều nghịch đảo cho 1 và không có giá trị nào khác.
amalloy

16

Không phải trong hầu hết các ngôn ngữ hàm, nhưng trong lập trình logic hoặc lập trình quan hệ, hầu hết các hàm bạn định nghĩa trên thực tế không phải là hàm mà là "quan hệ", và chúng có thể được sử dụng theo cả hai hướng. Xem ví dụ prolog hoặc kanren.


1
Hoặc Mercury , theo cách khác chia sẻ rất nhiều tinh thần của Haskell. - Điểm tốt, +1.
bùng binh trái vào

11

Những nhiệm vụ như thế này hầu như luôn không thể quyết định được. Bạn có thể có giải pháp cho một số chức năng cụ thể, nhưng không phải nói chung.

Ở đây, bạn thậm chí không thể nhận ra hàm nào có nghịch đảo. Trích dẫn Barendregt, HP Giải tích Lambda: Cú pháp và ngữ nghĩa của nó. Bắc Hà Lan, Amsterdam (1984) :

Một tập hợp các điều khoản lambda là không quan trọng nếu nó không phải là tập hợp rỗng hoặc không phải là tập hợp đầy đủ. Nếu A và B là hai tập hợp các số hạng lambda không đơn giản, rời rạc được đóng theo đẳng thức (beta), thì A và B không thể tách rời một cách đệ quy.

Hãy coi A là tập các số hạng lambda đại diện cho các hàm khả nghịch và B là phần còn lại. Cả hai đều không trống và đóng theo bình đẳng beta. Vì vậy, không thể quyết định liệu một hàm có khả nghịch hay không.

(Điều này áp dụng cho phép tính lambda không định kiểu. TBH Tôi không biết liệu đối số có thể được điều chỉnh trực tiếp với phép tính lambda đã nhập hay không khi chúng ta biết loại hàm mà chúng ta muốn đảo ngược. Nhưng tôi khá chắc chắn rằng nó sẽ như vậy giống.)


11

Nếu bạn có thể liệt kê miền của hàm và có thể so sánh các phần tử của phạm vi cho bằng nhau, bạn có thể - một cách khá đơn giản. Bằng cách liệt kê, tôi có nghĩa là có một danh sách tất cả các yếu tố có sẵn. Tôi sẽ gắn bó với Haskell, vì tôi không biết Ocaml (hoặc thậm chí làm thế nào để viết hoa nó đúng cách ;-)

Những gì bạn muốn làm là chạy qua các phần tử của miền và xem liệu chúng có bằng với phần tử của phạm vi mà bạn đang cố gắng đảo ngược hay không và lấy phần tử đầu tiên hoạt động:

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

Vì bạn đã nói rằng đó flà một phản ứng từ chối, nên nhất định phải có một và chỉ một phần tử như vậy. Tất nhiên, mẹo là đảm bảo rằng việc liệt kê miền của bạn thực sự đạt đến tất cả các phần tử trong một thời gian hữu hạn . Nếu bạn đang cố gắng đảo ngược một từ chối Integersang Integer, việc sử dụng [0,1 ..] ++ [-1,-2 ..]sẽ không hoạt động vì bạn sẽ không bao giờ chuyển đến các số âm. Nói một cách cụ thể, inv ([0,1 ..] ++ [-1,-2 ..]) (+1) (-3)sẽ không bao giờ mang lại giá trị.

Tuy nhiên, 0 : concatMap (\x -> [x,-x]) [1..]sẽ hoạt động, vì điều này chạy qua các số nguyên theo thứ tự sau [0,1,-1,2,-2,3,-3, and so on]. Quả thực inv (0 : concatMap (\x -> [x,-x]) [1..]) (+1) (-3)nhanh chóng trở lại -4!

Các Control.Monad.Omega gói có thể giúp bạn chạy qua danh sách các bộ etcetera trong một cách tốt; Tôi chắc rằng có nhiều gói như vậy nữa - nhưng tôi không biết chúng.


Tất nhiên, cách làm này khá thấp và thô bạo, chưa kể là xấu xí và không hiệu quả! Vì vậy, tôi sẽ kết thúc bằng một vài nhận xét về phần cuối cùng của câu hỏi của bạn, về cách 'viết' các tiểu sử. Loại hệ thống của Haskell không phải để chứng minh rằng một chức năng là một phản ứng - bạn thực sự muốn một cái gì đó giống như Agda cho điều đó - nhưng nó sẵn sàng tin tưởng bạn.

(Cảnh báo: sau mã chưa được kiểm tra)

Vì vậy, bạn có thể xác định một kiểu dữ liệu của Bijections giữa các kiểu ab:

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

cùng với bao nhiêu hằng số (nơi bạn có thể nói 'Tôi biết chúng là những đường nhị phân!') như bạn muốn, chẳng hạn như:

notBi :: Bi Bool Bool
notBi = Bi not not

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

và một số tổ hợp thông minh, chẳng hạn như:

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)

Tôi nghĩ bạn có thể làm invert (mapBi add1Bi) [1,5,6]và nhận được [0,4,5]. Nếu bạn chọn các tổ hợp của mình một cách thông minh, tôi nghĩ rằng số lần bạn phải viết một Bihằng số bằng tay có thể khá hạn chế.

Rốt cuộc, nếu bạn biết một hàm là một phép phân tích, hy vọng bạn sẽ có một bản phác thảo bằng chứng về thực tế đó trong đầu, mà phép đẳng cấu Curry-Howard sẽ có thể biến thành một chương trình :-)


6

Gần đây tôi đã giải quyết các vấn đề như thế này và không, tôi muốn nói rằng (a) nó không khó trong nhiều trường hợp, nhưng (b) nó không hiệu quả chút nào.

Về cơ bản, giả sử bạn có f :: a -> b, và đó fthực sự là một bjiection. Bạn có thể tính toán nghịch đảo f' :: b -> amột cách thực sự ngu ngốc:

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

Nếu flà một bijection và enumeratethực sự tạo ra tất cả các giá trị của a, thì cuối cùng bạn sẽ đạt được một giá trị anhư vậy f a == b.

Các loại có một Boundedvà một Enumthể hiện có thể được thực hiện một cách nhẹ nhàng RecursivelyEnumerable. Các cặp Enumerableloại cũng có thể được thực hiện 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

Tương tự đối với các Enumerableloại kết hợp:

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

Thực tế là chúng ta có thể làm điều này cho cả hai (,)Eithercó lẽ có nghĩa là chúng ta có thể làm điều đó cho bất kỳ kiểu dữ liệu đại số nào.


5

Không phải mọi hàm đều có nghịch đảo. Nếu bạn giới hạn cuộc thảo luận ở các hàm một đối một, thì khả năng đảo ngược một hàm tùy ý sẽ cho phép bẻ khóa bất kỳ hệ thống mật mã nào. Chúng ta phải hy vọng điều này không khả thi, ngay cả trên lý thuyết!


13
Bất kỳ hệ thống mật mã nào (ngoại trừ một vài hệ thống lẻ, chẳng hạn như một thời gian ngắn, không khả thi vì những lý do khác) đều có thể bị bẻ khóa bởi bạo lực. Điều đó không làm cho chúng trở nên kém hữu ích hơn và cũng không phải là một hàm đảo ngược không thực tế đắt tiền.

Nó có thực sự không? nếu bạn nghĩ về một chức năng mã hóa như String encrypt(String key, String text)không có chìa khóa, bạn vẫn sẽ không thể làm gì cả. CHỈNH SỬA: Cộng với những gì delnan đã nói.
mck

@MaciekAlbin Phụ thuộc vào mô hình tấn công của bạn. Ví dụ, các cuộc tấn công bản rõ được chọn có thể cho phép trích xuất khóa, sau đó sẽ cho phép tấn công các văn bản mật mã khác được mã hóa bằng khóa đó.

Ý tôi là "khả thi", điều gì đó có thể được thực hiện trong bất kỳ khoảng thời gian hợp lý nào. Ý tôi không phải là "có thể tính toán được" (tôi khá chắc chắn).
Jeffrey Scofield

@JeffreyScofield Tôi hiểu ý bạn. Nhưng tôi phải nói rằng, tôi đang bối rối bởi "khả thi trên lý thuyết" - không phải (định nghĩa của chúng tôi về) tính khả thi chỉ đề cập đến mức độ khó thực hiện trong thực tế?

5

Trong một số trường hợp, có thể tìm ra nghịch đảo của một hàm bijective bằng cách chuyển nó thành một biểu diễn tượng trưng. Dựa trên ví dụ này , tôi đã viết chương trình Haskell này để tìm nghịch đảo của một số hàm đa thức đơn giản:

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

Ví dụ này chỉ hoạt động với biểu thức số học, nhưng nó có thể được khái quát hóa để làm việc với danh sách.


4

Không, không phải tất cả các hàm đều có nghịch đảo. Ví dụ, nghịch đảo của hàm này sẽ là gì?

f x = 1

Hàm của bạn là một hằng số, ở đây nó là về các hàm bijective.
Soleil - Mathieu Prévot
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.