souffle-haskell-3.3.0: lib/Language/Souffle/Class.hs
{-# LANGUAGE DataKinds, UndecidableInstances, FlexibleContexts #-}
{-# LANGUAGE TypeFamilies, TypeOperators, TypeApplications #-}
-- | This module provides the top level API for Souffle related operations.
-- It makes use of Haskell's powerful typesystem to make certain invalid states
-- impossible to represent. It does this with a small type level DSL for
-- describing properties of the Datalog program (see the 'Program' and 'Fact'
-- typeclasses for more information).
--
-- The Souffle operations are exposed via 2 mtl-style interfaces
-- (see `MonadSouffle` and `MonadSouffleFileIO`) that allows them to be
-- integrated with existing monad transformer stacks.
--
-- This module also contains some helper type families for additional
-- type safety and user-friendly error messages.
module Language.Souffle.Class
( Program(..)
, ProgramOptions(..)
, Fact(..)
, FactOptions(..)
, Marshal.Marshal(..)
, Direction(..)
, ContainsInputFact
, ContainsOutputFact
, ContainsFact
, MonadSouffle(..)
, MonadSouffleFileIO(..)
) where
import Prelude hiding ( init )
import Control.Monad.Except
import Control.Monad.RWS.Strict
import Control.Monad.Reader
import Control.Monad.State
import Control.Monad.Writer
import Data.Proxy
import Data.Kind
import Data.Word
import GHC.TypeLits
import qualified Language.Souffle.Marshal as Marshal
import Type.Errors.Pretty
-- | A helper type family for checking if a specific Souffle `Program` contains
-- a certain `Fact`. Additionally, it also checks if the fact is marked as
-- either `Input` or `InputOutput`. This constraint will generate a
-- user-friendly type error if these conditions are not met.
type family ContainsInputFact prog fact :: Constraint where
ContainsInputFact prog fact = (ContainsFact prog fact, IsInput fact (FactDirection fact))
-- | A helper type family for checking if a specific Souffle `Program` contains
-- a certain `Fact`. Additionally, it also checks if the fact is marked as
-- either `Output` or `InputOutput`. This constraint will generate a
-- user-friendly type error if these conditions are not met.
type family ContainsOutputFact prog fact :: Constraint where
ContainsOutputFact prog fact = (ContainsFact prog fact, IsOutput fact (FactDirection fact))
type family IsInput (fact :: Type) (dir :: Direction) :: Constraint where
IsInput _ 'Input = ()
IsInput _ 'InputOutput = ()
IsInput fact dir = TypeError
( "You tried to use an " <> FormatDirection dir <> " fact of type " <> fact <> " as an input."
% "Possible solution: change the FactDirection of " <> fact
<> " to either 'Input' or 'InputOutput'."
)
type family IsOutput (fact :: Type) (dir :: Direction) :: Constraint where
IsOutput _ 'Output = ()
IsOutput _ 'InputOutput = ()
IsOutput fact dir = TypeError
( "You tried to use an " <> FormatDirection dir <> " fact of type " <> fact <> " as an output."
% "Possible solution: change the FactDirection of " <> fact
<> " to either 'Output' or 'InputOutput'."
)
type family FormatDirection (dir :: Direction) where
FormatDirection 'Output = "output"
FormatDirection 'Input = "input"
FormatDirection 'Internal = "internal"
-- | A helper type family for checking if a specific Souffle `Program` contains
-- a certain `Fact`. This constraint will generate a user-friendly type error
-- if this is not the case.
type family ContainsFact prog fact :: Constraint where
ContainsFact prog fact =
CheckContains prog (ProgramFacts prog) fact
type family CheckContains prog facts fact :: Constraint where
CheckContains prog '[] fact =
TypeError ("You tried to perform an action with a fact of type '" <> fact
<> "' for program '" <> prog <> "'."
% "The program contains the following facts: " <> ProgramFacts prog <> "."
% "It does not contain fact: " <> fact <> "."
% "You can fix this error by adding the type '" <> fact
<> "' to the ProgramFacts type in the Program instance for " <> prog <> ".")
CheckContains _ (a ': _) a = ()
CheckContains prog (_ ': as) b = CheckContains prog as b
-- | A typeclass for describing a datalog program.
--
-- Example usage (assuming the program was generated from path.dl
-- and contains 2 facts: Edge and Reachable):
--
-- @
-- data Path = Path -- Handle for the datalog program
--
-- instance Program Path where
-- type ProgramFacts Path = '[Edge, Reachable]
-- programName = const "path"
-- @
class Program a where
-- | A type level list of facts that belong to this program.
-- This list is used to check that only known facts are added to a program.
type ProgramFacts a :: [Type]
-- | Function for obtaining the name of a Datalog program.
-- This has to be the same as the name of the .dl file (minus the extension).
programName :: a -> String
-- | A helper data type, used in combination with the DerivingVia extension to
-- automatically generate code to bind Haskell to a Souffle Datalog program.
--
-- The following is an example how to bind to a Datalog program "path"
-- (saved as path.dl / path.cpp), that uses two facts called "edge" and
-- "reachable" (represented with the Edge and Reachable types):
--
-- @
-- data Path = Path
-- deriving Souffle.Program
-- via Souffle.ProgramOptions Path "path" '[Edge, Reachable]
-- @
--
-- See also: 'FactOptions'.
newtype ProgramOptions (prog :: Type) (progName :: Symbol) (facts :: [Type])
= ProgramOptions prog
instance KnownSymbol progName => Program (ProgramOptions prog progName facts) where
type ProgramFacts (ProgramOptions _ _ facts) = facts
programName = const $ symbolVal (Proxy @progName)
{-# INLINABLE programName #-}
-- | A typeclass for data types representing a fact in datalog.
--
-- Example usage:
--
-- @
-- instance Fact Edge where
-- type FactDirection Edge = 'Input
-- factName = const "edge"
-- @
class Marshal.Marshal a => Fact a where
-- | The direction or "mode" a fact can be used in.
-- This is used to perform compile-time checks that a fact is only used
-- in valid situations. For more information, see the 'Direction' type.
type FactDirection a :: Direction
-- | Function for obtaining the name of a fact
-- (has to be the same as described in the Datalog program).
--
-- It uses a 'Proxy' to select the correct instance.
factName :: Proxy a -> String
-- | A helper data type, used in combination with the DerivingVia extension to
-- automatically generate code to bind a Haskell datatype to a Souffle
-- Datalog fact.
--
-- The following is an example how to bind to a Datalog fact "edge"
-- that contains two symbols (strings in Haskell) that is an input (from the
-- Datalog point of view):
--
-- @
-- data Edge = Edge String String
-- deriving (Eq, Show, Generic)
-- deriving anyclass Souffle.Marshal
-- deriving Souffle.Fact
-- via Souffle.FactOptions Edge "edge" 'Souffle.Input
-- @
--
-- See also: 'ProgramOptions'.
newtype FactOptions (fact :: Type) (factName :: Symbol) (dir :: Direction)
= FactOptions fact
instance Marshal.Marshal fact => Marshal.Marshal (FactOptions fact name dir) where
push (FactOptions fact) = Marshal.push fact
{-# INLINABLE push #-}
pop = FactOptions <$> Marshal.pop
{-# INLINABLE pop #-}
instance ( Marshal.Marshal fact
, KnownSymbol factName
) => Fact (FactOptions fact factName dir) where
type FactDirection (FactOptions _ _ dir) = dir
factName = const $ symbolVal (Proxy @factName)
{-# INLINABLE factName #-}
-- | A datatype describing which operations a certain fact supports.
-- The direction is from the datalog perspective, so that it
-- aligns with ".decl" statements in Souffle.
data Direction
= Input
-- ^ Fact can only be stored in Datalog (using `addFact`/`addFacts`).
| Output
-- ^ Fact can only be read from Datalog (using `getFacts`/`findFact`).
| InputOutput
-- ^ Fact supports both reading from / writing to Datalog.
| Internal
-- ^ Supports neither reading from / writing to Datalog. This is used for
-- facts that are only visible inside Datalog itself.
-- | A mtl-style typeclass for Souffle-related actions.
class Monad m => MonadSouffle m where
-- | Represents a handle for interacting with a Souffle program.
--
-- The handle is used in all other functions of this typeclass to perform
-- Souffle-related actions.
type Handler m :: Type -> Type
-- | Helper associated type constraint that allows collecting facts from
-- Souffle in a list or vector. Only used internally.
type CollectFacts m (c :: Type -> Type) :: Constraint
-- | Helper associated type constraint that allows submitting facts to
-- Souffle. Only used internally.
type SubmitFacts m (a :: Type) :: Constraint
-- | Runs the Souffle program.
run :: Handler m prog -> m ()
-- | Sets the number of CPU cores this Souffle program should use.
setNumThreads :: Handler m prog -> Word64 -> m ()
-- | Gets the number of CPU cores this Souffle program should use.
getNumThreads :: Handler m prog -> m Word64
-- | Returns all facts of a program. This function makes use of type inference
-- to select the type of fact to return.
getFacts :: (Fact a, ContainsOutputFact prog a, CollectFacts m c)
=> Handler m prog -> m (c a)
-- | Searches for a fact in a program.
-- Returns 'Nothing' if no matching fact was found; otherwise 'Just' the fact.
--
-- Conceptually equivalent to @List.find (== fact) \<$\> getFacts prog@,
-- but this operation can be implemented much faster.
findFact :: (Fact a, ContainsOutputFact prog a, Eq a, SubmitFacts m a)
=> Handler m prog -> a -> m (Maybe a)
-- | Adds a fact to the program.
addFact :: (Fact a, ContainsInputFact prog a, SubmitFacts m a)
=> Handler m prog -> a -> m ()
-- | Adds multiple facts to the program. This function could be implemented
-- in terms of 'addFact', but this is done as a minor optimization.
addFacts :: (Foldable t, Fact a, ContainsInputFact prog a, SubmitFacts m a)
=> Handler m prog -> t a -> m ()
instance MonadSouffle m => MonadSouffle (ReaderT r m) where
type Handler (ReaderT r m) = Handler m
type CollectFacts (ReaderT r m) c = CollectFacts m c
type SubmitFacts (ReaderT r m) a = SubmitFacts m a
run = lift . run
{-# INLINABLE run #-}
setNumThreads prog = lift . setNumThreads prog
{-# INLINABLE setNumThreads #-}
getNumThreads = lift . getNumThreads
{-# INLINABLE getNumThreads #-}
getFacts = lift . getFacts
{-# INLINABLE getFacts #-}
findFact prog = lift . findFact prog
{-# INLINABLE findFact #-}
addFact fact = lift . addFact fact
{-# INLINABLE addFact #-}
addFacts facts = lift . addFacts facts
{-# INLINABLE addFacts #-}
instance (Monoid w, MonadSouffle m) => MonadSouffle (WriterT w m) where
type Handler (WriterT w m) = Handler m
type CollectFacts (WriterT w m) c = CollectFacts m c
type SubmitFacts (WriterT w m) a = SubmitFacts m a
run = lift . run
{-# INLINABLE run #-}
setNumThreads prog = lift . setNumThreads prog
{-# INLINABLE setNumThreads #-}
getNumThreads = lift . getNumThreads
{-# INLINABLE getNumThreads #-}
getFacts = lift . getFacts
{-# INLINABLE getFacts #-}
findFact prog = lift . findFact prog
{-# INLINABLE findFact #-}
addFact fact = lift . addFact fact
{-# INLINABLE addFact #-}
addFacts facts = lift . addFacts facts
{-# INLINABLE addFacts #-}
instance MonadSouffle m => MonadSouffle (StateT s m) where
type Handler (StateT s m) = Handler m
type CollectFacts (StateT s m) c = CollectFacts m c
type SubmitFacts (StateT s m) a = SubmitFacts m a
run = lift . run
{-# INLINABLE run #-}
setNumThreads prog = lift . setNumThreads prog
{-# INLINABLE setNumThreads #-}
getNumThreads = lift . getNumThreads
{-# INLINABLE getNumThreads #-}
getFacts = lift . getFacts
{-# INLINABLE getFacts #-}
findFact prog = lift . findFact prog
{-# INLINABLE findFact #-}
addFact fact = lift . addFact fact
{-# INLINABLE addFact #-}
addFacts facts = lift . addFacts facts
{-# INLINABLE addFacts #-}
instance (MonadSouffle m, Monoid w) => MonadSouffle (RWST r w s m) where
type Handler (RWST r w s m) = Handler m
type CollectFacts (RWST r w s m) c = CollectFacts m c
type SubmitFacts (RWST r w s m) a = SubmitFacts m a
run = lift . run
{-# INLINABLE run #-}
setNumThreads prog = lift . setNumThreads prog
{-# INLINABLE setNumThreads #-}
getNumThreads = lift . getNumThreads
{-# INLINABLE getNumThreads #-}
getFacts = lift . getFacts
{-# INLINABLE getFacts #-}
findFact prog = lift . findFact prog
{-# INLINABLE findFact #-}
addFact fact = lift . addFact fact
{-# INLINABLE addFact #-}
addFacts facts = lift . addFacts facts
{-# INLINABLE addFacts #-}
instance MonadSouffle m => MonadSouffle (ExceptT e m) where
type Handler (ExceptT e m) = Handler m
type CollectFacts (ExceptT e m) c = CollectFacts m c
type SubmitFacts (ExceptT e m) a = SubmitFacts m a
run = lift . run
{-# INLINABLE run #-}
setNumThreads prog = lift . setNumThreads prog
{-# INLINABLE setNumThreads #-}
getNumThreads = lift . getNumThreads
{-# INLINABLE getNumThreads #-}
getFacts = lift . getFacts
{-# INLINABLE getFacts #-}
findFact prog = lift . findFact prog
{-# INLINABLE findFact #-}
addFact fact = lift . addFact fact
{-# INLINABLE addFact #-}
addFacts facts = lift . addFacts facts
{-# INLINABLE addFacts #-}
-- | A mtl-style typeclass for Souffle-related actions that involve file IO.
class MonadSouffle m => MonadSouffleFileIO m where
-- | Load all facts from files in a certain directory.
loadFiles :: Handler m prog -> FilePath -> m ()
-- | Write out all facts of the program to CSV files in a certain directory
-- (as defined in the Souffle program).
writeFiles :: Handler m prog -> FilePath -> m ()
instance MonadSouffleFileIO m => MonadSouffleFileIO (ReaderT r m) where
loadFiles prog = lift . loadFiles prog
{-# INLINABLE loadFiles #-}
writeFiles prog = lift . writeFiles prog
{-# INLINABLE writeFiles #-}
instance (Monoid w, MonadSouffleFileIO m) => MonadSouffleFileIO (WriterT w m) where
loadFiles prog = lift . loadFiles prog
{-# INLINABLE loadFiles #-}
writeFiles prog = lift . writeFiles prog
{-# INLINABLE writeFiles #-}
instance MonadSouffleFileIO m => MonadSouffleFileIO (StateT s m) where
loadFiles prog = lift . loadFiles prog
{-# INLINABLE loadFiles #-}
writeFiles prog = lift . writeFiles prog
{-# INLINABLE writeFiles #-}
instance (MonadSouffleFileIO m, Monoid w) => MonadSouffleFileIO (RWST r w s m) where
loadFiles prog = lift . loadFiles prog
{-# INLINABLE loadFiles #-}
writeFiles prog = lift . writeFiles prog
{-# INLINABLE writeFiles #-}
instance MonadSouffleFileIO m => MonadSouffleFileIO (ExceptT s m) where
loadFiles prog = lift . loadFiles prog
{-# INLINABLE loadFiles #-}
writeFiles prog = lift . writeFiles prog
{-# INLINABLE writeFiles #-}