Haskell — невозможное возможно?

в 18:55, , рубрики: haskell, магия, невозможное возможно, функциональное программирование, метки: , ,

Известно, что задача определения того, истинна ли некоторая функция Integer -> Bool хотя бы для одного числа вычислительно неразрешима. Однако, нечто, на первый взгляд кажущееся как раз таким оракулом (а именно, функцией (Integer -> Bool) -> Maybe Integer) будет описано в этой статье.

Для начала, зададим свой тип натуральных чисел, практически дословно следуя их обычному математическому определению (почему это нужно будет видно в дальнейшем):

data Nat = Zero | Succ Nat (Eq, Ord, Show)

Другими словами, натуральное число — это либо ноль, либо некоторое натуральное число, увеличенное на единицу (Succ от слова successor).

Также, для удобства, определим основные операции (сложение, умножение, конвертация из Integer) над числами в таком представлении:

instance Num Nat where
    Zero + y = y
    Succ x + y = Succ (x + y)

    Zero * y = Zero
    Succ x * y = y + (x * y)

    fromInteger 0 = Zero
    fromInteger n = Succ (fromInteger (n-1))


Пока, вроде бы, ничего особенного в плане отличий этого типа от обычного Integer.

Вспомним, что мы хотим функцию вида (Nat -> Bool) -> Maybe Nat, результатом которой будет число, на котором поданная на вход функция возвращает True, или Nothing если такого числа нет. Первым приближением может быть, например, подобная функция:

lyingSearch :: (Nat -> Bool) -> Nat
lyingSearch f | f Zero = Zero
              | otherwise = Succ (lyingSearch (f . Succ))

На самом деле, почти очевидно, что в случае существования искомого числа эта функция вернёт верный ответ. Действительно, если f Zero == True, то возвращаемым значением будет Zero — верно. Иначе функция вернёт x+1, где x — значение, на котором истинна функция f(x+1) — тоже верно.
Однако, не зря у этой функции название lyingSearch: в случае, когда искомого числа нет, функция будет на каждом шаге уходить в рекурсию и вернёт, по сути, бесконечность: Succ (Succ (Succ (..., где вложенность никогда не окончится. Из-за ленивости Haskell это — нормальная ситуация. Но ведь бесконечность не является искомым ответом — следовательно в данном случае функция «солжёт».

Что интересно, полностью работающее решение можно сделать на базе приведённой выше функции lyingSearch. Рассмотрим функцию search, определённую так:

search f | f possibleMatch = Just possibleMatch
         | otherwise = Nothing
    where
    possibleMatch = lyingSearch f

На первый взгляд непонятно, как такое будет работать, и будет ли. Проверим на простых примерах:

ghci> search (x -> x*x == 16) -- такое работало и для lyingSearch
Just (Succ (Succ (Succ (Succ Zero))))
ghci> search (x -> x*x == 15)
Nothing

То есть, функция search верно определила, что не существует натурального числа с квадратом равным пятнадцати.

На самом деле, если разобраться, то всё просто. Получив от lyingSearch возможный результат (который всегда является допустимым значением типа Nat) мы просто подаём его на вход функции f и проверяем возвращаемое значение. Если искомое число существует, то (как уже выяснено ранее) possibleMatch — как раз это число, и следовательно проверка пройдёт успешно. Иначе, т.к. f завершается для любого входного значения, мы получим False и вернём Nothing.

Функция search действительно работает для любого предиката (функции Nat->Bool), и завершается за конечное время (разумеется, при условии, что f также завершается для любого значения типа Nat). Однако, условие завершения f для любого переданного аргумента очень сильное, и именно оно сильно ограничивает множество допустимых предикатов: например, при f x = x*x + 1 == x будет бесконечный цикл. Казалось бы, что здесь не так, ведь такая функция завершается для любого числа? Оказывается, для любого кроме уже упомянутой бескочности: слева и справа от знака равенства будут бесконечно вложенные Succ (Succ (Succ (..., и соответственно невозможно будет определить, равны ли левая и правая части. Именно по этой причине невозможно использовать данную функцию для создания аналогичной для типа Integer.

Теперь уже можно простыми словами объяснить, почему и каким образом всё работает для всегда завершающихся функций. Ведь если f завершается на переданной ей бесконечности, то есть последовательности Succ (Succ (Succ (..., значит она в любом случае использует (раскрывает) не более некоторого фиксированного числа конструкторов Succ.

Аналогичным по сути способом можно создавать функции вроде search и для других типов. Относительно простым примером также являются действительные числа, представленные каждое в виде бесконечного списка цифр (см. статью Seemingly Impossible Functional Programs). На hackage есть обобщающий пакет infinite-search, который предоставляет соответствующую монаду и сопутствующие функции.

P.S.: эта статья является несколько дополненным «пересказом» Searchable Data Types, поэтому как перевод не помечена.

Автор: chersanya

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js