simplistic-generics-2.0.0: src/Generics/Simplistic/Unify.hs
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE ScopedTypeVariables #-}
-- |Constraint-based unification algorithm for 'Holes'
module Generics.Simplistic.Unify
( -- * Substitution
Subst , substEmpty , substInsert , substLkup , substApply
-- * Unification
, UnifyErr(..) , unify , unify_ , unifyWith , minimize
) where
import Data.List (sort)
import qualified Data.Map as M
import Control.Monad.Except
import Control.Monad.State
import Control.Monad.Writer hiding (All)
import Control.Monad.Cont
import Unsafe.Coerce
import Generics.Simplistic.Deep
import Generics.Simplistic.Util
-- |Unification can return succesfully or find either
-- a 'OccursCheck' failure or a 'SymbolClash' failure.
data UnifyErr kappa fam phi :: * where
-- |The occurs-check fails when the variable in question
-- occurs within the term its supposed to be unified with.
OccursCheck :: [Exists phi]
-> UnifyErr kappa fam phi
-- |A symbol-clash is thrown when the head of the
-- two terms is different and neither is a variabe.
SymbolClash :: Holes kappa fam phi at
-> Holes kappa fam phi at
-> UnifyErr kappa fam phi
-- |A substitution is but a map; the existential quantifiers are
-- necessary to ensure we can reuse from "Data.Map"
--
-- Note that we must be able to compare @Exists phi@. This
-- comparison needs to work heterogeneously and it must
-- return 'EQ' only when the contents are in fact equal.
-- Even though we only need this instance for "Data.Map"
--
-- A typical example for @phi@ is @Const Int at@, representing
-- a variable. The @Ord@ instance would then be:
--
-- > instance Ord (Exists (Const Int)) where
-- > compare (Exists (Const x)) (Exists (Const y))
-- > = compare x y
--
type Subst kappa fam phi
= M.Map (Exists phi) (Exists (Holes kappa fam phi))
-- |Empty substitution
substEmpty :: Subst kappa fam phi
substEmpty = M.empty
-- |Looks a value up in a substitution, see 'substInsert'
substLkup :: (Ord (Exists phi))
=> Subst kappa fam phi -- ^
-> phi at
-> Maybe (Holes kappa fam phi at)
substLkup sigma var =
case M.lookup (Exists var) sigma of
Nothing -> Nothing
-- In case we found something, it must be of the same
-- type as what we got because we only insert
-- well-typed things.
-- TODO: Use our sameTy method to remove the unsafeCoerce
Just (Exists t) -> Just $ unsafeCoerce t
-- |Applies a substitution to a term once; Variables not in the
-- support of the substitution are left untouched.
substApply :: (Ord (Exists phi))
=> Subst kappa fam phi -- ^
-> Holes kappa fam phi at
-> Holes kappa fam phi at
substApply sigma = holesJoin
. holesMap (\v -> maybe (Hole v) id $ substLkup sigma v)
-- |Inserts a point in a substitution. Note how the index of
-- @phi@ /must/ match the index of the term being inserted.
-- This is important when looking up terms because we must
-- 'unsafeCoerce' the existential type variables to return.
--
-- Please, always use this insertion function; or, if you insert
-- by hand, ensure thetype indices match.
substInsert :: (Ord (Exists phi))
=> Subst kappa fam phi -- ^
-> phi at
-> Holes kappa fam phi at
-> Subst kappa fam phi
substInsert sigma v x = M.insert (Exists v) (Exists x) sigma
-- |Unification is done in a monad.
type UnifyM kappa fam phi
= StateT (Subst kappa fam phi) (Except (UnifyErr kappa fam phi))
-- |Attempts to unify two 'Holes', but ignores
-- which error happened when they could not be unified.
unify_ :: (All Eq kappa , Ord (Exists phi) , EqHO phi)
=> Holes kappa fam phi at -- ^
-> Holes kappa fam phi at
-> Maybe (Subst kappa fam phi)
unify_ a = either (const Nothing) Just
. runExcept . unify a
-- |Attempts to unify two 'Holes'
unify :: (All Eq kappa , Ord (Exists phi) , EqHO phi)
=> Holes kappa fam phi at -- ^
-> Holes kappa fam phi at
-> Except (UnifyErr kappa fam phi)
(Subst kappa fam phi)
unify = unifyWith substEmpty
-- |Attempts to unify two 'Holes' with an already existing
-- substitution
unifyWith :: (All Eq kappa , Ord (Exists phi) , EqHO phi)
=> Subst kappa fam phi -- ^ Starting subst
-> Holes kappa fam phi at
-> Holes kappa fam phi at
-> Except (UnifyErr kappa fam phi)
(Subst kappa fam phi)
unifyWith sigma x y = execStateT (unifyM x y) sigma
-- Actual unification algorithm; In order to improve efficiency,
-- we first register all equivalences we need to satisfy,
-- then on 'mininize' we do the occurs-check.
unifyM :: forall kappa fam phi at
. (All Eq kappa , EqHO phi , Ord (Exists phi))
=> Holes kappa fam phi at
-> Holes kappa fam phi at
-> UnifyM kappa fam phi ()
unifyM x y = do
_ <- getEquivs x y
s <- get
case minimize s of
Left vs -> throwError (OccursCheck vs)
Right s' -> put s'
where
getEquivs :: Holes kappa fam phi b
-> Holes kappa fam phi b
-> UnifyM kappa fam phi ()
getEquivs p q = void $ holesMapM (uncurry' getEq) (lgg p q)
getEq :: Holes kappa fam phi b
-> Holes kappa fam phi b
-> UnifyM kappa fam phi (Holes kappa fam phi b)
getEq p (Hole var) = record_eq var p >> return p
getEq p@(Hole var) q = record_eq var q >> return p
getEq p q | eqHO p q = return p
| otherwise = throwError (SymbolClash p q)
-- Whenever we see a variable being matched against a term
-- we record the equivalence. First we make sure we did not
-- record such equivalence yet, otherwise, we recursively thin
record_eq :: phi b -> Holes kappa fam phi b -> UnifyM kappa fam phi ()
record_eq var q = do
sigma <- get
case substLkup sigma var of
-- First time we see 'var', we instantiate it and get going.
Nothing -> when (not $ eqHO q (Hole var))
$ modify (\s -> substInsert s var q)
-- It's not the first time we thin 'var'; previously, we had
-- that 'var' was supposed to be p'. We will check whether it
-- is the same as q, if not, we will have to thin p' with q.
Just q' -> unless (eqHO q' q)
$ void $ getEquivs q q'
-- |The minimization step performs the /occurs check/ and removes
-- unecessary steps, returning an idempodent substitution when
-- successful. For example;
--
-- > sigma = fromList
-- > [ (0 , bin 1 2)
-- > , (1 , bin 4 4) ]
--
-- Then, @minimize sigma@ will return @fromList [(0 , bin (bin 4 4) 2) , (1 , bin 4 4)]@
-- This returns @Left vs@ if occurs-check fail for variables @vs@.
--
minimize :: forall kappa fam phi . (Ord (Exists phi))
=> Subst kappa fam phi -- ^
-> Either [Exists phi] (Subst kappa fam phi)
minimize sigma =
let sigma' = breakCycles inj proj $ removeIds proj sigma
in whileM sigma' [] $ \s _
-> M.fromList <$> (mapM (secondF (exMapM (go sigma'))) (M.toList s))
where
inj :: Exists phi -> Exists (Holes kappa fam phi)
inj = exMap Hole
proj :: Exists (Holes kappa fam phi) -> Maybe (Exists phi)
proj (Exists (Hole phi)) = Just $ Exists phi
proj _ = Nothing
secondF :: (Functor m) => (a -> m b) -> (x , a) -> m (x , b)
secondF f (x , a) = (x,) <$> f a
-- The actual engine of the 'minimize' function is thinning the
-- variables that appear in the image of the substitution under production.
-- We use the writer monad solely to let us know whether some variables have
-- been substituted in this current term. After one iteration
-- of the map where no variable is further refined, we are done.
go :: Subst kappa fam phi
-> Holes kappa fam phi at
-> Writer [Exists phi] (Holes kappa fam phi at)
go ss = holesRefineHolesM $ \var -> do
case substLkup ss var of
Nothing -> return (Hole var)
Just r -> tell [Exists var]
>> return r
-- | Just like nub; but works on a sorted list
mnub :: (Ord a) => [a] -> [a]
mnub [] = []
mnub [x] = [x]
mnub (x:y:ys)
| x == y = mnub (y:ys)
| otherwise = x : mnub (y:ys)
-- We loop while there is work to be done or no progress
-- was done.
whileM :: (Ord (Exists phi))
=> a -> [Exists phi] -> (a -> [Exists phi] -> Writer [Exists phi] a)
-> Either [Exists phi] a
whileM a xs f = do
let (x' , xs') = runWriter (f a xs)
if null xs'
then return x'
else if (mnub (sort xs') == mnub (sort xs))
then Left xs'
else whileM x' xs' f
-- |Removes the keys that project to themselves according to
-- the provided projection.
--
-- > removeIds id $ M.fromList [(0,0), (1,2) , (3,4)]
-- > = M.fromList [(1,2),(3,4)]
--
removeIds :: forall a b
. (Ord a) => (b -> Maybe a) -> M.Map a b -> M.Map a b
removeIds proj = M.filterWithKey (\a b -> not $ Just a == proj b)
-- |Will make sure there are no cycles in the map as per the
-- provided function. For example,
--
-- > breakCycles id Just $ M.fromList [(0,1) , (1,2) , (2,0)]
-- > = M.fromList [(1,0),(2,0)]
--
-- Usefull when the maps represent some sort of equivalence; the function
-- essentially collapses the equivalence class.
breakCycles :: forall a b
. (Ord a) => (a -> b) -> (b -> Maybe a) -> M.Map a b -> M.Map a b
breakCycles inj proj m0
= let (flattenedCycles , m') = runState (dropCycles m0) M.empty
in M.union flattenedCycles m'
where
dropCycles :: M.Map a b -> State (M.Map a b) (M.Map a b)
dropCycles m = case findCycle m of
Nothing -> return m
Just (a , cyc) -> do
modify (M.union cyc)
dropCycles (M.delete a (m M.\\ cyc))
cycleFor :: a -> M.Map a b -> Maybe (M.Map a b)
cycleFor a0 m = M.lookup a0 m >>= proj >>= go M.empty
where
go :: M.Map a b -> a -> Maybe (M.Map a b)
go aux a'
| a' == a0 || a' `M.member` aux = return aux
| otherwise = M.lookup a' m >>= proj >>= go (M.insert a' (inj a0) aux)
findCycle :: M.Map a b -> Maybe (a , M.Map a b)
findCycle m = (`runCont` id) $ callCC $
\exit -> (>> return Nothing) . flip mapM_ (M.keys m) $
\a -> case cycleFor a m of
Nothing -> return ()
Just r -> exit $ Just (a , r)