Люк (luqui) представил, пожалуй, самый элегантный способ думать о проблеме. Тем не менее, его кодировка требует, чтобы вы вручную получали большие полосы обхода для каждого такого правила перезаписи, которое вы хотите создать.
Композиции Бьорна Брингерта из Шаблон для почти составных функций может упростить эту задачу, особенно если у вас есть несколько таких проходов нормализации, которые вам нужно написать. Обычно он написан с использованием Applicative или типов ранга 2, но для простоты здесь я отложу:
Учитывая ваш тип данных
data LogicalExpression
= Var Char
| Neg LogicalExpression
| Conj LogicalExpression LogicalExpression
| Disj LogicalExpression LogicalExpression
| Impl LogicalExpression LogicalExpression
deriving (Show)
Мы можем определить класс, используемый для поиска подвыражений не верхнего уровня:
class Compos a where
compos' :: (a -> a) -> a -> a
instance Compos LogicalExpression where
compos' f (Neg e) = Neg (f e)
compos' f (Conj a b) = Conj (f a) (f b)
compos' f (Disj a b) = Disj (f a) (f b)
compos' f (Impl a b) = Impl (f a) (f b)
compos' _ t = t
Например, мы могли бы устранить все последствия:
elimImpl :: LogicalExpression -> LogicalExpression
elimImpl (Impl a b) = Disj (Not (elimImpl a)) (elimImpl b)
elimImpl t = compos' elimImpl t -- search deeper
Тогда мы можем применить его, как это делает Луки выше, для поиска отрицательных союзов и дизъюнкций. И поскольку, как указывает Люк, вероятно, лучше выполнить все распределение отрицаний за один проход, мы также включим нормализацию отрицательного подтекста и исключение двойного отрицания, получая формулу в нормальной форме отрицания (при условии, что мы уже устранены последствия)
nnf :: LogicalExpression -> LogicalExpression
nnf (Neg (Conj a b)) = Disj (nnf (Neg a)) (nnf (Neg b))
nnf (Neg (Disj a b)) = Conj (nnf (Neg a)) (nnf (Neg b))
nnf (Neg (Neg a)) = nnf a
nnf t = compos' nnf t -- search and replace
Ключ - последняя строка, в которой говорится, что если ни один из вышеперечисленных случаев не совпадает, отправляйтесь на поиски подвыражений, где вы можете применить это правило. Кроме того, поскольку мы вставляем Neg
в термины, а затем нормализуем их, вы должны получить только отрицательные переменные на листьях, поскольку все другие случаи, когда Neg
предшествует другому конструктору, будут нормализованы.
Более продвинутая версия будет использовать
import Control.Applicative
import Control.Monad.Identity
class Compos a where
compos :: Applicative f => (a -> f a) -> a -> f a
compos' :: Compos a => (a -> a) -> a -> a
compos' f = runIdentity . compos (Identity . f)
и
instance Compos LogicalExpression where
compos f (Neg e) = Neg <$> f e
compos f (Conj a b) = Conj <$> f a <*> f b
compos f (Disj a b) = Disj <$> f a <*> f b
compos f (Impl a b) = Impl <$> f a <*> f b
compos _ t = pure t
Это не поможет в вашем конкретном случае здесь, но полезно позже, если вам нужно вернуть несколько переписанных результатов, выполнить IO
или иным образом участвовать в более сложных действиях в вашем правиле перезаписи.
Возможно, вам понадобится использовать это, если, например, вы хотите попытаться применить законы деМоргана в любом подмножестве мест, где они применяются, а не следовать обычной форме.
Обратите внимание, что независимо от того, какую функцию вы переписываете, какую программу вы используете, или даже направленности потока информации во время обхода, определение compos
нужно давать только один раз для каждого типа данных.