tie-knot-0.2: src/Data/Knot.hs
{-
This file is part of tie-knot.
tie-knot is free software: you can redistribute it and/or modify it under
the terms of the GNU Lesser General Public License as published by the Free
Software Foundation, either version 3 of the License, or (at your option)
any later version.
tie-knot is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for
more details.
You should have received a copy of the GNU Lesser General Public License
along with tie-knot. If not, see <http://www.gnu.org/licenses/>.
-}
{-# LANGUAGE TypeFamilies, FlexibleContexts, FlexibleInstances #-}
-- | Module for tying the knot on data structures that reference each other by
-- some kind of keys. The 'tie' function replaces all such references with the actual
-- value, creating possibly recursive or cyclic data structures.
--
-- The module re-exports a part of the recursion-schemes package.
--
-- An example how to construct a structure with circular dependencies:
--
-- > data Person = Person { name :: String, loves :: [Person] }
-- > -- Define a variant of Person where the recursive type
-- > -- is given as a parameter and the embedding function.
-- > data Loves t = Loves { _name :: String, _loves :: [t] }
-- > type instance Base Person = Loves
-- > instance Unfoldable Person where
-- > embed ~(Loves n ps) = Person n ps
-- >
-- > -- The easisest way to get 'Foldable' + 'Functor' is to implement
-- > -- 'Traversable' and then just use the default implementations.
-- > instance T.Traversable Loves where
-- > traverse f (Loves n ns) = Loves n <$> T.traverse f ns
-- >
-- > instance Functor Loves where
-- > fmap = T.fmapDefault
-- > instance F.Foldable Loves where
-- > foldMap = T.foldMapDefault
-- >
-- > -- Let's create a person with cicrular dependencies:
-- > alice :: Person
-- > alice = fromJust . Map.lookup "Alice" .
-- > tie' . Map.fromList . map (\l -> (_name l, l)) $ lst
-- > where
-- > lst = [ Loves "Alice" ["Bob", "cat"]
-- > , Loves "Bob" ["Alice"]
-- > -- you may disagree, but the cat thinks of itself as Person
-- > , Loves "cat" ["cat"]
-- > ]
module Data.Knot (tie, tie', isConsistent, RefMap, TieError(..), Base, Unfoldable, embed) where
import Control.Monad
import Control.Monad.Error
import qualified Data.Foldable as F
import Data.Map (Map)
import qualified Data.Map as Map
import Data.Monoid
import Data.Maybe
import Data.Functor.Foldable
-- | Represents a set of data 'v' that reference each other
-- using keys of type 'k'.
type RefMap k v = Map k (v k)
-- | Possible errors when tying the knot.
data TieError k
= MissingKey k k -- ^ A value with key k1 referenced non-existent key k2.
deriving (Show, Eq, Ord)
-- | Check the loader for consistency, i.e. if all referenced keys
-- have a corresponding value. Values need to implement 'Foldable'
-- that traverses over all referenced keys.
isConsistent :: (Ord k, F.Foldable v, Functor v)
=> RefMap k v -- ^ The loader to check.
-> Either (TieError k) (RefMap k v) -- ^ The loader argument or an error.
isConsistent l = maybe (Right l) Left . getFirst $
Map.foldrWithKey (\k -> mappend . keysOk k) mempty l
where
keysOk k = F.foldMap (\r -> First $ if (Map.member r l)
then Nothing
else (Just (MissingKey k r)) )
-- | Helper function for anamorphisms.
ana' :: Unfoldable t => (s -> Base t s) -> Base t s -> t
ana' f = embed . fmap (ana f)
-- | Ties the knot without checking consistency.
-- If the references are inconsistent, an exception is raised.
tie' :: (Ord k, Unfoldable v) => RefMap k (Base v) -> Map k v
tie' m = Map.map f m
where
-- (k -> v k) -> v k -> Fix v
f = ana' $ \k -> fromJust (Map.lookup k m)
-- | Checks consistency by calling 'isConsistent' and then and ties the knot using 'tie''.
tie :: (Ord k, F.Foldable (Base v), Unfoldable v) => RefMap k (Base v) -> Either (TieError k) (Map k v)
tie = liftM tie' . isConsistent