Упрощение GADT с Uniplate
Я пытаюсь ответить на этот вопрос, используя uniplate
как я и предлагал, но единственное решение, которое я до сих пор придумала, довольно уродливо.
Это кажется довольно распространенной проблемой, поэтому я хотел знать, есть ли более элегантное решение.
По сути, у нас есть GADT, который разрешает либо Expression Int
или же Expression Bool
(без учета codataIf = If (B True) codataIf codataIf
):
data Expression a where
I :: Int -> Expression Int
B :: Bool -> Expression Bool
Add :: Expression Int -> Expression Int -> Expression Int
Mul :: Expression Int -> Expression Int -> Expression Int
Eq :: Expression Int -> Expression Int -> Expression Bool
And :: Expression Bool -> Expression Bool -> Expression Bool
Or :: Expression Bool -> Expression Bool -> Expression Bool
If :: Expression Bool -> Expression a -> Expression a -> Expression a
И (в моей версии проблемы) мы хотим иметь возможность оценивать дерево выражений снизу вверх, используя простую операцию для объединения листьев в новый лист:
step :: Expression a -> Expression a
step = \case
Add (I x) (I y) -> I $ x + y
Mul (I x) (I y) -> I $ x * y
Eq (I x) (I y) -> B $ x == y
And (B x) (B y) -> B $ x && y
Or (B x) (B y) -> B $ x || y
If (B b) x y -> if b then x else y
z -> z
У меня были некоторые трудности с использованием DataDeriving
вывести Uniplate
а также Biplate
экземпляры (которые, возможно, должны были быть красным флагом), поэтому я бросил свой собственный Uniplate
случаи для Expression Int
, Expression Bool
, а также Biplate
случаи для (Expression a) (Expression a)
, (Expression Int) (Expression Bool)
, а также (Expression Bool) (Expression Int)
,
Это позволило мне придумать эти обходы снизу вверх:
evalInt :: Expression Int -> Expression Int
evalInt = transform step
evalIntBi :: Expression Bool -> Expression Bool
evalIntBi = transformBi (step :: Expression Int -> Expression Int)
evalBool :: Expression Bool -> Expression Bool
evalBool = transform step
evalBoolBi :: Expression Int -> Expression Int
evalBoolBi = transformBi (step :: Expression Bool -> Expression Bool)
Но так как каждый из них может сделать только одно преобразование (объединить Int
листья или Bool
оставляет, но не любой), они не могут сделать полное упрощение, но должны быть связаны вручную:
λ example1
If (Eq (I 0) (Add (I 0) (I 0))) (I 1) (I 2)
λ evalInt it
If (Eq (I 0) (I 0)) (I 1) (I 2)
λ evalBoolBi it
If (B True) (I 1) (I 2)
λ evalInt it
I 1
λ example2
If (Eq (I 0) (Add (I 0) (I 0))) (B True) (B False)
λ evalIntBi it
If (Eq (I 0) (I 0)) (B True) (B False)
λ evalBool it
B True
Мой хакерский обходной путь должен был определить Uniplate
экземпляр для Either (Expression Int) (Expression Bool)
:
type WExp = Either (Expression Int) (Expression Bool)
instance Uniplate WExp where
uniplate = \case
Left (Add x y) -> plate (i2 Left Add) |* Left x |* Left y
Left (Mul x y) -> plate (i2 Left Mul) |* Left x |* Left y
Left (If b x y) -> plate (bi2 Left If) |* Right b |* Left x |* Left y
Right (Eq x y) -> plate (i2 Right Eq) |* Left x |* Left y
Right (And x y) -> plate (b2 Right And) |* Right x |* Right y
Right (Or x y) -> plate (b2 Right Or) |* Right x |* Right y
Right (If b x y) -> plate (b3 Right If) |* Right b |* Right x |* Right y
e -> plate e
where i2 side op (Left x) (Left y) = side (op x y)
i2 _ _ _ _ = error "type mismatch"
b2 side op (Right x) (Right y) = side (op x y)
b2 _ _ _ _ = error "type mismatch"
bi2 side op (Right x) (Left y) (Left z) = side (op x y z)
bi2 _ _ _ _ _ = error "type mismatch"
b3 side op (Right x) (Right y) (Right z) = side (op x y z)
b3 _ _ _ _ _ = error "type mismatch"
evalWExp :: WExp -> WExp
evalWExp = transform (either (Left . step) (Right . step))
Теперь я могу сделать полное упрощение:
λ evalWExp . Left $ example1
Left (I 1)
λ evalWExp . Right $ example2
Right (B True)
Но количество error
и обертывание / распаковка, которые я должен был сделать, чтобы сделать эту работу, просто делает ее неуместной и неправильной для меня.
Есть ли правильный способ решить эту проблему сuniplate
?
1 ответ
Не существует правильного способа решить эту проблему с помощью uniplate, но есть правильный способ решить эту проблему с помощью того же механизма. Библиотека uniplate не поддерживает uniplating тип данных с видом * -> *
, но мы можем создать другой класс, чтобы приспособиться к этому. Вот небольшая минимальная библиотека uniplate для типов * -> *
, Он основан на текущей версии git Uniplate
что было изменено для использования Applicative
вместо Str
,
{-# LANGUAGE RankNTypes #-}
import Control.Applicative
import Control.Monad.Identity
class Uniplate1 f where
uniplate1 :: Applicative m => f a -> (forall b. f b -> m (f b)) -> m (f a)
descend1 :: (forall b. f b -> f b) -> f a -> f a
descend1 f x = runIdentity $ descendM1 (pure . f) x
descendM1 :: Applicative m => (forall b. f b -> m (f b)) -> f a -> m (f a)
descendM1 = flip uniplate1
transform1 :: Uniplate1 f => (forall b. f b -> f b) -> f a -> f a
transform1 f = f . descend1 (transform1 f)
Теперь мы можем написать Uniplate1
экземпляр для Expression
:
instance Uniplate1 Expression where
uniplate1 e p = case e of
Add x y -> liftA2 Add (p x) (p y)
Mul x y -> liftA2 Mul (p x) (p y)
Eq x y -> liftA2 Eq (p x) (p y)
And x y -> liftA2 And (p x) (p y)
Or x y -> liftA2 Or (p x) (p y)
If b x y -> pure If <*> p b <*> p x <*> p y
e -> pure e
Этот экземпляр очень похож на emap
Функция, которую я написал в своем ответе на исходный вопрос, за исключением того, что этот экземпляр помещает каждый элемент в Applicative
Functor
, descend1
просто поднимает свой аргумент в Identity
а также runIdentity
результат, делая desend1
идентичный emap
, таким образом transform1
идентично postmap
из предыдущего ответа.
Теперь мы можем определить reduce
с точки зрения transform1
,
reduce = transform1 step
Этого достаточно, чтобы запустить пример:
"reduce"
If (And (B True) (Or (B False) (B True))) (Add (I 1) (Mul (I 2) (I 3))) (I 0)
I 7