Haskell Bindings - GSoC 2015

From OpenCog
Jump to: navigation, search

This page gathers the main ideas and advances on the GSoC 2015 project: "Haskell Bindings for OpenCog" [1].

We are going to work on a forked repo of the atomspace, so you can follow this on [2].

For general information on Haskell bindings see: Haskell

For Haddock documentation: opencog-atomspace-0.1.0.0

Student: MarcosPividori

Mentor: Nil Geisweiller

Abstract

The aim of this project it to develop Haskell bindings for the main interface of the OpenCog software. Existing bindings for Python and Scheme are in use. It is possible to write Cognitive processes in these languages, and interact with the efficient C++ OpenCog core. So, we aim to have comparable Haskell bindings.

Technical Considerations

The main OpenCog applications, the AtomSpace and CogServer, are developed in C++. Unfortunately, there isn't any appropiate tool for calling C++ functions and creating/deleting objects from Haskell ([3] [4] [5]). So, the general approach is to wrap the C++ code to C, and use the FFI (Foreign Function Interface [6]) to call the resulting C functions from Haskell. Going that way, the initial idea is to develop a wrapper for the main class AtomSpace, exporting its main functionalities as C functions and then call them from haskell code using FFI.

As pointed out in [7], an AtomSpace is useful in itself for module development, automated testing, and for people learning about OpenCog. So, it is important to offer an API for interacting with it independently of using or not the CogServer interface. According to that, we are going to work on developing a Haskell library that could be linked with the libAtomSpace and be used for creating independent executables.

I think that taking this approach, directly accessing C code from haskell, will probably be faster than translating into Scheme and then being interpreted (as proposed in some discussions on this topic [8]).

We are developing an initial example interface for the atomspace with some functions to create/delete a new atomspace and add new nodes (GitHub: [9]). The haskell Code includes an initial proposal of the modules to be used on a cabal package. (OpenCog.AtomSpace....)

AtomSpace Environment

The main idea is to build programs that work on an AtomSpace on the Monad 'AtomSpace'. Then, we can run this programs with the function runOnNewAtomSpace:

runOnNewAtomSpace :: AtomSpace a -> IO a

It creates a new C++ atomspace behind, does all the computation, and finally deletes it.

The AtomSpace data type is defined as:

type AtomSpace = ReaderT AtomSpaceRef IO

ReaderT is a monad transformer, so in fact:

AtomSpace a = ReaderT { runReaderT :: AtomSpaceRef -> IO a }	 

The interpretation of runReaderT in this case is: "given an atomspace in memory, it reduces to performing IO actions with a result of type a"

Because of the use of the monad IO, we can lift IO actions inside de monad AtomSpace, through the use of liftIO. For example:

import OpenCog.AtomSpace.Api
import Control.Monad.IO.Class

prog :: AtomSpace ()
prog = do
        liftIO $ putStrLn "hello world"
        ...
 
main :: IO ()
main = runOnNewAtomSpace prog

Initial Implementation

We developed:

  • haskell-atomspace: a shared library, C wrapper for the atomspace main class. It is built and installed like all the atomspace libraries.
  • opencog-atomspace: a haskell package that links with the haskell-atomspace library and offers an API for haskell programming on the atomspace.

Haskell Program --imports--> opencog-atomspace Haskell Library --links_with--> haskell-atomspace library ---links_with--> atomspace library.

Requirements and Installation

Look at the Haskell page.

IO Monad

By using FFI, we are working with a mutable object outside the Haskell pure environment. Our state is represented by the monad IO. I mean the state of the atomspace object in memory outside.

I read old discussions on this topic suggesting the use of the monad State.

" The Haskell type State describes functions that consume a state and produce a tuple that contains a result along with the new state after the result has been extracted.

   newtype State s a = State { runState :: s -> (a, s) }

Here, s is the type of the state, and a the type of the produced result. " from [5]

As we are working with a mutable object outside Haskell pure environment, our state is represented by the monad IO, so we will use a "runReaderT" function a little different from "runState".

   data AtomSpace a = AtomSpace {runReaderT :: AtomSpaceRef -> IO a}

Our runReaderT function will return the state wrapped in the monad IO, I mean the state of the atomspace object in memory outside.

We can not get rid of the monad IO because we can not completely represent the state of the atomspace in Haskell.

On the other hand, if we want to work purely, without maintaining a state outside the Haskell pure environment, we should re implement the AtomSpace completely in Haskell. Then, we could use a pure monad State without IO.

CogServer Integration

In the future, when developing a Haskell shell plugin for CogServer (using GHCi), we can offer a similar function runOnCogServerAtomSpace that will enable us to run programs on the specific instance of the AtomSpace running on the CogServer. This seems to be a simple task, something like

   runOnCogServerAtomSpace :: AtomSpace a -> IO a
   runOnCogServerAtomSpace m = runReaderT m cogServerAtomSpaceRef

Where cogServerAtomSpaceRef is a reference to the actual instance of the AtomSpace in the CogServer, that we will provide to GHCi. (something like: AtomSpaceRef pointerToAtomSpaceObjectInMemory)

Atoms Data

  • ATOM
    • Mutable:
      • Attention Value
      • Truth Value
    • Immutable and unique:
      • Handle ID
  • LINK
    • Immutable and unique:
      • Type
      • Outgoing
  • NODE
    • Immutable and unique:
      • Type
      • Name

AtomSpace Api

  • insert : inserts an atom to the AtomSpace. (~ similar to INSERT/UPDATE of SQL)
  • remove : removes an atom from the AtomSpace. (~ similar to DELETE of SQL)
  • get : gets an atom from the AtomSpace. In fact, it looks for a specific atom in the atomspace and retrieves it, which means retrieving the mutable data:
Truth Val and Attention Val. (~ similar to SELECT of SQL)

In future, we could add other similar functions to search by node name, link outgoing, type, etc. (getting multiple atoms)

Atoms Notation

(From: Engineering General Intelligence Part 2 "2.2 Denoting Atoms") General sintaxis:

 LinkType ListOfAtoms(outgoing) <optional truth value> <<optional att value>>
 NodeType NodeName              <optional truth value> <<optional att value>>

We can use indent notation to avoid ambiguity when writing Link's outgoing lists.

Atoms Representation

So, we want to develop an EDSL, Embeeded Domain Specific Language (ref), to interact with Atoms and the AtomSpace, embeeded in Haskell. To develop an EDSL, we are going to work around a data type for representing Atoms, let's name it "Atom".

Also, we want to take advantages of Haskell type system to specify type restrictions on the type of the Atoms and how they combine together, so we need to incorporate these restrictions in the construction of the data type "Atom".

The main goals on Atom representation is:

  • We want to use a simple EDSL to create new atoms, as similar as possible to the Atoms Notation described above ("Atoms Notation"). For example:
    ... do
          insert $ AndLink
                       ConceptNode "something"
                       ConceptNode "other thing"
                       <0.5,0.5>
          ...
  • We want to be able to do Pattern Matching over the atoms types to follow the atom relation, from one to another. For example:
    ... case node of
          AndLink a1 a2 tv -> do something
          ConceptNode name -> do something
          ...

For creating new atoms and working with atoms data types, we have different options:

  • Work directly with the Data Constructors, for example, if we define the AndLink data constructor as:
       AndLink :: Atom -> Atom -> Maybe TruthVal -> Atom
Then we can use it to create a new node as:
      AndLink (ConceptNode "something" Nothing)
              (ConceptNode "other thing" Nothing) (Just (0.5,0.5))
    ---------------------------------------------------------------------------
    {-# LANGUAGE GADTs #-}

    data Atom a where
        ConceptNode :: String -> Maybe TruthVal -> Atom a
        Link :: Atom b -> Atom c -> Maybe TruthVal -> Atom a

    main = let nod = Link
                        (ConceptNode "Joan" Nothing)
                        (ConceptNode "Juan" (Just (0.5,0.4))
                        (Just (0.3,0.2))
            in
              case nod of
                ConceptNode _ _ -> putStrLn "We have a ConceptNode"
                Link _ _ _      -> putStrLn "We have a Link"
    --------------------------------------------------------------------------
The problem with this approach is that we are limited to use the Haskell sintaxis, we can not use indentation notation, we have to use parenthesis to specify asociation, we can not make truth values optional, we always have to specify this parameter as Nothing or (Just value). I mean,we are using the internal representation.
As advantage, we are using the same representation that we can access through pattern matching, data type constructors and internal representation,so we build and unpack atoms using the same notation.
  • To improve the syntax of the embedded language, we could avoid the usage of data constructors directly, and replace them by specific functions to build atom types:
    ---------------------------------------------------------------------------
    {-== LANGUAGE PostfixOperators, GADTs ==-}

    data Atom a where
        ConceptNode :: String -> TV -> Atom a
        Link :: Atom b -> Atom c -> TV -> Atom a

    conceptNode :: String -> Atom a
    conceptNode s = ConceptNode s Nothing

    link :: Atom a -> Atom b -> Atom c
    link s1 s2 = Link s1 s2 Nothing

    (.<) :: Atom a -> Double -> Atom a
    (.<) (ConceptNode s _) i = ConceptNode s (Just i)
    (.<) (Link s1 s2 _) i = Link s1 s2 (Just i)

    infixl 5 .<

    main = let nod = (link
                        (conceptNode "Joan")
                        (conceptNode "Juan" .<2)
                        .<4
                     )
            in do
                 case nod of
                   ConceptNode _ _ -> putStrLn "We have a ConceptNode"
                   Link _ _ _      -> putStrLn "We have a Link"
    --------------------------------------------------------------------------
So, we avoid to specify the TruthValue when not necessary. But we have to deal with Types constructors on pattern matching, so we are using 2 differents notations.
  • Other approach, is to use Template Haskell to wrap the Data Constructors with an Atom Notation similar to the one described above. We could write for example:
       ... [atom| AndLink
                       ConceptNode "something"
                       ConceptNode "other thing"
                       <0.5,0.5>
           |]
This would be converted to the internal representation:
     AndLink (ConceptNode "something") (ConceptNode "other thing") (Just (0.5,0.5))
But when doing pattern matching, we have to use the Data constructors, such as:
       ... case node of
             AndLink a1 a2 tv -> do something
             ConceptNode name -> do something
             ...
So, we should use code as similar as possible between Data Constructors and template haskell, to make it simple.
    ---------------------------------------------------------------------------
    program :: AtomSpace ()
    program = do
                  liftIO $ putStrLn "Let's add some new nodes:"
                  insert [atom| EvaluationLink <0.9>
                                    PredicateNode "is"
                                    Concept "John"
                                    Concept "John" |]
                  s <- get [atom| EvaluationLink
                                    PredicateNode "is"
                                    Concept "John"
                                    Concept "John" |]
                  case s of
                    PredicateNode "is" _ _ -> liftIO $ putStrLn "It's Predicate Node"
                    Concept "John" _ _     -> liftIO $ putStrLn "It's Concept Node"
                    _                      -> liftIO $ putStrLn "It's other type"
    ---------------------------------------------------------------------------

Anyway, I think we can start with the Data Constructors and once everything is working fine, we can add support for Template Haskell to use the original Atom Notation embedded in Haskell.

Atom Data Type Definition

(We have considered many different approaches on defining data types for atoms representation. We discuss the different options availables and their advantages and disadvantages on this branch. For now, the best option seems to be GADTs.)

When developing the data type definitions for atoms, we have some main goals:

  • We want to impose type restrictions in how atoms connect between them, I mean, we want to constrain the class of atoms that a certain link type includes in its outgoing set, established by the hierarchy:
     Atom
      |   \
     Node Link
      |      \
     Concept  Evaluation
      ...
Everywhere we need a Node, we can place an SchemaNode or a ConceptNode, but we can't place a ListLink for example. We want the compiler to check this.
For example, an ExecutionLink, has in its outgoing set: first has an schema node, then a list link and then any atom type.
We want the compiler to check this type conditions.
So, we need to incorporate this hierarchy to the haskell type environment:
  • We need to go upward and downward in class hierarchy. For example, if we have some atom of class Schema, we need to be able to use it as a Node (upward). Also, if we have an atom of class Node, some times we need to know the real atom type, for example SchemaNode, GroundedSchemaNode, etc (downward).
  • Also, we need a common data type to group all atom types, let's call it AtomGen. We need this, for example, when defining a ListLink, where we need to define a list of atoms: [AtomGen], or when defining a query functionality on the atomspace like: getByName :: Name -> AtomSpace [AtomGen]
So, we need a way to group all atoms types in a same data type. But this process has to be reversible, I mean, we need to be able to get the specific atom back from AtomGen. For example, when examining the items of a ListLink.

Looking for a solution to this, the first approaches are:

FIRST OPTION

Code: GitHub

We define a different data type for each atom type, and an associated type class, in order to impose the type constraints.

    data ConceptNode   = ConceptNode AtomName (Maybe TV)
    data PredicateNode = PredicateNode AtomName
    data SchemaNode    = SchemaNode AtomName
    data GroundedSchemaNode = GroundedSchemaNode AtomName
    data ListLink      = ListLink [AtomGen]
    data ExecutionLink = forall s l a. (IsSchema s, IsList l,IsAtom a)
                       => ExecutionLink s l a

And we define the proper class hierarchy between them, through Type Classes. (This could be done automatically from the types.script file)

    class IsAtom a where
        toAtom   :: a -> AtomGen
    class IsAtom a   => IsNode a
    class IsAtom a   => IsLink a
    class IsNode a   => IsConcept a
    class IsNode a   => IsPredicate a
    class IsNode a   => IsSchema a
    class IsSchema a => IsGroundedSchema a
    class IsLink a   => IsList a
    class IsLink a   => IsExecution a

By defining them this way, the compiler knows than an Schema is also a Node and is also an Atom, etc. I mean, the compiler can follow hierarchy upwards. So, we define the instances of these classes:

    instance IsAtom PredicateNode where
        toAtom = GenPredicate
    instance IsNode PredicateNode
    instance IsPredicate PredicateNode

    instance IsAtom ConceptNode where
        toAtom = GenConcept
    instance IsNode ConceptNode
    instance IsConcept ConceptNode

    instance IsAtom SchemaNode where
        toAtom = GenSchema
    instance IsNode SchemaNode
    instance IsSchema SchemaNode

    instance IsAtom GroundedSchemaNode where
        toAtom = GenGroundedSchema
    instance IsNode GroundedSchemaNode
    instance IsSchema GroundedSchemaNode
    instance IsGroundedSchema GroundedSchemaNode

    instance IsAtom ListLink where
        toAtom = GenList
    instance IsLink ListLink
    instance IsList ListLink

    instance IsAtom ExecutionLink where
        toAtom = GenExecution
    instance IsLink ExecutionLink
    instance IsExecution ExecutionLink

And finally, we define a general data type:

    data AtomGen = GenConcept        ConceptNode
                 | GenPredicate      PredicateNode
                 | GenSchema         SchemaNode
                 | GenGroundedSchema GroundedSchemaNode
                 | GenList           ListLink
                 | GenExecution      ExecutionLink

So let's analyse the main goals:

  • Atom hierarchy: OK. Combining data types and type classes. It works really well. For example:
    -- Type checking OK:
       ex1 = ExecutionLink
          (GroundedSchemaNode "some-fun")
          (ListLink [ toAtom $ ConceptNode "Arg1" someTv
                    , toAtom $ ConceptNode "Arg2" someTv
                    ])
          (ConceptNode "res" someTv)

    -- Type checking error:
       ex2 = ExecutionLink
          (ConceptNode "some-conc" Nothing) -- This type isn't instance of IsSchema
          (PredicateNode "some-pred")       -- This type isn't instance of IsList
          (ConceptNode "res" someTv)
  • Upward/Downward:
Upward: Type classes, automatically.
Downward: if we have some instance of IsA and we know A is an atom type, then we can convert it to the AtomGen general data type with toAtom, and then we can do pattern matching over AtomGen to know the specific atom type. For example:
    case ex1 of
        ExecutionLink x _ _ -> case toAtom x of
            GenGroundedSchema _ -> print "We have a GSchema"
            GenSchema         _ -> print "We have a Schema"
  • General data type:
We define AtomGen as a general atom type, with a different constructor for each atom type.

GADTs OPTION:

Code: GitHub

With GADTs, we can group together all atom types in a same data type Atom a, where the "a" type variable is used as a phantom type to carry the atom type information and impose type constraints. So, we define empty data types for each atom type:

    data ConceptT
    data SchemaT
    data GroundedSchemaT
    data ListT
    data ExecutionT

And we define the proper class hierarchy between them, through Type Classes. (This could be done automatically from the types.script file)

    class IsAtom
    class IsAtom a => IsNode a
    class IsAtom a => IsLink a
    class IsNode a => IsConcept a
    class IsNode a => IsSchema a
    class IsSchema a => IsGroundedSchema a
    class IsLink a => IsList a
    class IsLink a => IsExecution a

By defining them this way, the compiler knows than an Schema is also a Node and is also an Atom, etc. I mean, the compiler can follow hierarchy upwards. So, we define the instances of these classes:

    instance IsAtom ConceptT
    instance IsNode ConceptT
    instance IsConcept ConceptT

    instance IsAtom SchemaT
    instance IsNode SchemaT
    instance IsSchema SchemaT

    instance IsAtom GroundedSchemaT
    instance IsNode GroundedSchemaT
    instance IsSchema GroundedSchemaT
    instance IsGroundedSchema GroundedSchemaT

    instance IsAtom ListT
    instance IsLink ListT
    instance IsList ListT

    instance IsAtom ExecutionT
    instance IsLink ExecutionT
    instance IsExecution ExecutionT

Now, we can define the Atom data type, imposing type restrictions in how atoms relate between them through phantom types and type classes!

    data Atom a where
        ConceptNode    :: AtomName -> Maybe TV -> Atom ConceptT
        SchemaNode     :: AtomName -> Atom SchemaT
        GroundedSchemaNode :: AtomName -> Atom GroundedSchemaT
        ListLink       :: [AtomGen] -> Atom ListT
        ExecutionLink  :: (IsSchema s,IsList l,IsAtom a)
                       => Atom s -> Atom l -> Atoma -> Atom ExecutionT

    AtomGen = forall a. AtomGen (Atom a)

So let's analyse the main goals:

  • Atom hierarchy: OK. Combining phantom types, type classes and GADTs.
It works really well. For example:
    -- Type checking OK:
    ex = ExecutionLink
       (GroundedSchemaNode "some-fun")
       (ListLink [ AtomGen $ ConceptNode "Arg1" someTv
                 , AtomGen $ ConceptNode "Arg2" someTv
                 ])
       (ConceptNode "res" someTv)

    -- Type checking error:
    exx = ExecutionLink
       (ConceptNode "some-conc" Nothing) -- ConceptT type isn't instance of IsSchema
       (PredicateNode "some-pred")       -- PredicateT type isn't instance of IsList
       (ConceptNode "res" someTv)
  • Upward/Downward:
Upward: Type classes, automatically!
Downward: Pattern Matching on GADTs constructors!
As we are using GADTs to group all atom types we have the advantages of doing pattern matching on the constructors to know which specific atom type we have. For example:
    case ex1 of
        ExecutionLink x _ _ -> case x of
            GroundedSchemaNode _ -> print "We have a GSchema"
            SchemaNode         _ -> print "We have a Schema"
  • General data type:
We define a general data type AtomGen using Existential Quantifiers over the phantom type 'a' in the type Atom a, and adding a constructor AtomGen.


  • Also, with GADTs, we avoid using a different constructor for each type of Atom inside AtomGen.
Now, we can do pattern matching on an atom type, for example, on the items of a list:
    case l of
        List x:xs -> case x of
            AtomGen (Concept c _) -> we have a concept
            AtomGen (Predicate p) -> we have a predicate

FINAL OPTION: GADTs + TypeFamilies + ConstraintKinds

Finally, to reduce boilerplate code, we used some extensions:

      data AtomType = AnchorT
                    | AtomT
                    | ConceptT
                    | ...
Which are promoted to the type level.
  • TypeFamilies we moved from using type classes to start using type families to record the hierarchical relation between atom types:
      type family Is (a :: AtomType) (b :: AtomType) :: Bool where
          Is AndT AndT = True
          Is AndT AtomT = True
          Is AndT LinkT = True
          Is AndT UnorderedT = True
          Is ConceptT ConceptT = True
          Is ConceptT AtomT = True
          Is ConceptT NodeT = True
          ...

      type family Up a :: [AtomType] where
          Up AtomT = '[AtomT]
          Up AndT = '[AndT, AtomT, LinkT, UnorderedT]
          Up ConceptT = '[ConceptT, AtomT, NodeT]
          ...
So, "Is a b" holds "True" iff "a inherits from b", and "Up a" holds a list of ancestors of atom type "a" (included itself).
      -- 'IsParent' is a contraint on being 'b' an ancestor of 'a'.
      type IsParent a b = Is a b ~ 'True
Also, to make the compiler follow hierarchy upwards, we need to provide information about all their ancestors, so we define:
      -- 'ParConst' builds a list of constraints to assert that all the members of
      -- the list are ancestors of a.
      type family ParConst a (b :: [AtomType]) :: Constraint where
          ParConst a '[]      = 'True ~ 'True
          ParConst a (b ': c) = (IsParent a b,ParConst a c)

      -- | '<~' builds a list of constraints to assert that all the ancestors of b        
      -- (included b itself) are ancestors of a.                                       
      infix 9 <~                                                                       
      type a <~ b = (Typeable a,ParConst a (Up b))
  • GADTs So using the constraints defined above, we define the Atom type (the main data type to represent the different types of atoms):
      data Atom (a :: AtomType) where

        -- PredicateNode is a node, so it is composed of its name and truth value.
        PredicateNode       :: AtomName -> TVal -> Atom PredicateT

        -- AndLink is of unlimited arity, so it takes a list of atoms and a truth value.
        AndLink             :: TVal -> [AtomGen] -> Atom AndT

        -- EvaluationLink takes a Predicate and a list of atoms.
        EvaluationLink      :: (p <~ PredicateT,l <~ ListT) =>
                               TVal -> Atom p -> Atom l -> Atom EvaluationT

        -- ListLink takes a list of Atoms.
        ListLink            :: [AtomGen] -> Atom ListT

        -- ExecutionLink takes first an atom that ''inherits from'' SchemaT.
        -- (It could be a SchemaNode, a GroundedSchemaNode, etc)
        ExecutionLink       :: (s <~ SchemaT,l <~ ListT,a <~ AtomT) =>
                               Atom s -> Atom l -> Atom a -> Atom ExecutionT 

        -- BindLink takes Variables, and 2 general atoms.
        BindLink            :: (v <~ VariableT,p <~ AtomT,q <~ AtomT) =>
                               Atom v -> Atom p -> Atom q -> Atom BindT
        ... (etc)
So, the compiler will ensure that this relation between atoms is preserved. For example:
        -- Type checking Ok.
        ex1 :: Atom ExecutionT
        ex1 = ExecutionLink
                (GroundedSchemaNode "some-fun")
                (ListLink |> ConceptNode "Arg1" (stv 1 1)
                          \> ConceptNode "Arg2" (stv 1 1) )
                (ConceptNode "res" (stv 1 1))

        -- Type checking error.
        ex2 :: Atom ExecutionT
        ex2 = ExecutionLink
                (ConceptNode "some-fun" (stv 1 1)) -- ConceptNodeT isn't a Schema.
                (ListLink |> ConceptNode "Arg1" (stv 1 1)
                          \> ConceptNode "Arg2" (stv 1 1) )
                (ConceptNode "res" (stv 1 1))


        -- Type checking Ok.
        findAnimals1 :: Atom BindT
        findAnimals1 = BindLink
                          (VariableNode "$var")
                          (InheritanceLink noTv
                              (VariableNode "$var")
                              (ConceptNode "animal" noTv)
                          )
                          (VariableNode "$var")

        -- Type checking error.
        findAnimals2 :: Atom BindT
        findAnimals2 = BindLink
                          (GroundedSchemaNode "some-fun") -- This is not a Variable.
                          (ListLink |> ConceptNode "Arg1" (stv 1 1)
                                    \> ConceptNode "Arg2" (stv 1 1) )
                          (ConceptNode "res" (stv 1 1))

Template Haskell and automatically generation of code

In actual implementation of Haskell bindings, four files depend on atomtype definitions, and are expected to be updated in case of modifications in atom types list:

  • AtomTypes.hs - Here we define all phantom types, and their hierarchical relation.
  • Filter.hs - Here we define some filters necessary to the serializing step.
  • Internal.hs - Here we do all the serializing of atoms to communicate with OpenCog framework.
  • Types.hs - Here we define the GADts, the main data type that users use to work on the atomspace.

For the first 2 files, we have enough with the information provided now by the file: atom_types.script. We are using Template Haskell to read atom types specification (Atom type hierarchy) from that file and automatically generate proper data types, type classes, type families, etc. You can see that code in the module: OpenCog.AtomSpace.Template.

On the other hand, for the Internal.hs and Types.hs files, we need more information about the members of an outgoing set. So they must be manually modified. (See: Haskell#Adding_new_Atom_Types)

Examples

You can find many examples on: examples/haskell

Similar Haskell libraries

You can find main DB packages in : Databases_and_Persistence

In particular Persistent can be a really good source of inspiration because: it is high-level, it does heavy usage of types to allows strong type guarantees, it uses template haskell to remove boilerplate code, etc.

See Also