packages feed

hasklepias-0.13.0: src/FeatureCompose.hs

{-|
Module      : Define and evaluate Features 
Description : Defines the Feature type and its component types, constructors, 
              and class instances
Copyright   : (c) NoviSci, Inc 2020
License     : BSD3
Maintainer  : bsaul@novisci.com

-}
{-# OPTIONS_HADDOCK hide #-}

{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE Safe #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE OverloadedStrings #-}

module FeatureCompose(
  -- * Features and FeatureData
    FeatureData
  , MissingReason(..)
  , Feature
  , FeatureN
  , featureDataL
  , featureDataR
  , missingBecause
  , makeFeature
  , getFeatureData
  , getFData
  , getData
  , getDataN
  , getNameN
  , nameFeature

  -- * Defining and evaluating Features
  , Definition(..)
  , Define(..)
  , DefineA(..)
  , Eval
  , eval

  , HasAttributes(..)
) where

import safe Control.Applicative        ( Applicative(..)
                                        , liftA3, (<$>) )
import safe Control.Monad              ( Functor(..), Monad(..)
                                       , (=<<), join, liftM, liftM2, liftM3)
import safe Data.Either                ( Either(..) )
import safe Data.Eq                    ( Eq(..) )
import safe Data.Foldable              ( Foldable(foldr) )
import safe Data.Function              ( ($), (.) )
import safe Data.List                  ( (++) )
import safe Data.Proxy                 ( Proxy(..) )
import safe Data.Text                  ( Text, pack )
import safe Data.Traversable           ( Traversable(..) )
import safe GHC.Generics               ( Generic )
import safe GHC.Show                   ( Show(show) )
import safe GHC.TypeLits               ( KnownSymbol, Symbol, symbolVal )
import safe FeatureCompose.Attributes
{- | 
Defines the reasons that a @'FeatureData'@ value may be missing. Can be used to
indicate the reason that a @'Feature'@'s data was unable to be derived or does
not need to be derived. 
-}
{- tag::missingReason[] -}
data MissingReason =
    InsufficientData -- ^ Insufficient information available to derive data.
  | Other Text -- ^ User provided reason for missingness
{- end::missingReason[] -}
  deriving (Eq, Show, Generic)

{- | 
The @FeatureData@ type is a container for an (almost) arbitrary type @d@ that can
have a "failed" or "missing" state. The failure is represented by the @'Left'@ of 
an @'Either'@, while the data @d@ is contained in the @'Either'@'s @'Right'@.

To construct a successful value, use @'featureDataR'@. A missing value can be 
constructed with @'featureDataL'@ or its synonym @'missingBecause'@.

-}
{- tag::featureData[] -}
newtype FeatureData d = MkFeatureData { 
    getFeatureData :: Either MissingReason d  -- ^ Unwrap FeatureData.
  }
{- end::featureData[] -}
  deriving (Eq, Show, Generic)

-- | Creates a non-missing 'FeatureData'. Since @'FeatureData'@ is an instance of
-- @'Applicative'@, @'pure'@ is also a synonym of for @'featureDataR'@.
-- 
-- >>> featureDataR "aString"
-- MkFeatureData (Right "aString")
-- >>> featureDataR (1 :: P.Int)
-- MkFeatureData (Right 1)
-- 
-- >>> featureDataR ("aString", (1 :: P.Int))
-- MkFeatureData (Right ("aString",1))
--
featureDataR :: d -> FeatureData d
featureDataR = MkFeatureData . Right

-- | Creates a missing 'FeatureData'.
-- 
-- >>> featureDataL (Other "no good reason") :: FeatureData P.Int
-- MkFeatureData (Left (Other "no good reason"))
--
-- >>> featureDataL (Other "no good reason") :: FeatureData Text
-- MkFeatureData (Left (Other "no good reason"))
--
featureDataL :: MissingReason -> FeatureData d
featureDataL = MkFeatureData . Left

-- | A synonym for 'featureDataL'.
missingBecause :: MissingReason -> FeatureData d
missingBecause = featureDataL

{- FeatureData instances -}

-- | Transform ('fmap') @'FeatureData'@ of one type to another.
--
-- >>> x = featureDataR (1 :: P.Int)
-- >>> :type x
-- >>> :type ( fmap show x )
-- x :: FeatureData Int
-- ( fmap show x ) :: FeatureData String
-- 
-- Note that 'Left' values are carried along while the type changes:
--
-- >>> x = ( featureDataL InsufficientData ) :: FeatureData P.Int
-- >>> :type x
-- >>> x
-- >>> :type ( fmap show x )
-- >>> fmap show x 
-- x :: FeatureData Int
-- MkFeatureData {getFeatureData = Left InsufficientData}
-- ( fmap show x ) :: FeatureData String
-- MkFeatureData {getFeatureData = Left InsufficientData}
--
instance Functor FeatureData where
  fmap f (MkFeatureData x) = MkFeatureData (fmap f x)

instance Applicative FeatureData where
  pure = featureDataR
  liftA2 f (MkFeatureData x) (MkFeatureData y) = MkFeatureData (liftA2 f x y)

instance Monad FeatureData where
  (MkFeatureData x) >>= f =
      case fmap f x of
         Left l  -> MkFeatureData $ Left l
         Right v -> v

instance Foldable FeatureData where
  foldr f x (MkFeatureData z) = foldr f x z

instance Traversable FeatureData where
  traverse f (MkFeatureData z) = MkFeatureData <$> traverse f z

{- | 
The @'Feature'@ is an abstraction for @name@d @d@ata, where the @name@ is a
*type*. Essentially, it is a container for @'FeatureData'@ that assigns a @name@
to the data.

Except when using @'pure'@ to lift data into a @Feature@, @Feature@s can only be
derived from other @Feature@ via a @'Definition'@.
-}
{- tag::feature[] -}
newtype (KnownSymbol name) => Feature name d =
  MkFeature { getFData :: FeatureData d }
{- end::feature[] -}
  deriving (Eq)

-- | A utility for constructing a @'Feature'@ from @'FeatureData'@.
-- Since @name@ is a type, you may need to annotate the type when using this
-- function.
--
-- >>> makeFeature (pure "test") :: Feature "dummy" Text
-- "dummy": MkFeatureData {getFeatureData = Right "test"}
--
makeFeature :: (KnownSymbol name) => FeatureData d -> Feature name d
makeFeature = MkFeature

-- | A utility for getting the (inner) @'FeatureData'@ content of a @'Feature'@.
getData :: Feature n d -> Either MissingReason d
getData (MkFeature x) = getFeatureData x

{- Feature instances -}
instance (KnownSymbol name, Show a) => Show (Feature name a) where
  show (MkFeature x) = show (symbolVal (Proxy @name)) ++ ": " ++ show x

instance Functor (Feature name) where
  fmap f (MkFeature x) = MkFeature (fmap f x)

instance Applicative (Feature name) where
  pure x = MkFeature (pure x)
  liftA2 f (MkFeature x) (MkFeature y) = MkFeature (liftA2 f x y)

instance Foldable (Feature name) where
  foldr f x (MkFeature t) = foldr f x t

instance Traversable (Feature name) where
  traverse f (MkFeature x) = MkFeature <$> traverse f x

instance Monad (Feature name) where
   (MkFeature x) >>= f =
        case fmap f x of
          MkFeatureData (Left l)  -> MkFeature $ MkFeatureData (Left l)
          MkFeatureData (Right r) ->  r

{- |
The @'FeatureN'@ type is similar to @'Feature'@ where the @name@ is included
as a @Text@ field. This type is mainly for internal purposes in order to collect
@Feature@s of the same type @d@ into a homogeneous container like a @'Data.List'@.
-}
data FeatureN d = MkFeatureN {
        getNameN :: Text  -- ^ Get the name of a @FeatureN@.
      , getDataN :: FeatureData d -- ^ Get the data of a @FeatureN@
      } deriving (Eq, Show)

-- | A utility for converting a @'Feature'@ to @'FeatureN'@.
nameFeature :: forall name d . (KnownSymbol name) => Feature name d -> FeatureN d
nameFeature (MkFeature d) = MkFeatureN (pack $ symbolVal (Proxy @name)) d

{- | A @Definition@ can be thought of as a lifted function. Specifically, the
@'define'@ function takes an arbitrary function (currently up to three arguments)
and returns a @Defintion@ where the arguments have been lifted to a new domain.

For example, here we take @f@ and lift to to a function of @Feature@s.

@
f :: Int -> String -> Bool
f i s 
  | 1 "yes" = True
  | otherwise = FALSE

myFeature :: Definition (Feature "A" Int -> Feature "B" String -> Feature "C" Bool )
myFeature = define f
@

See @'eval'@ for evaluating @Defintions@. 

-}

data Definition d where
  D1  :: (b -> a) -> Definition (f1 b -> f0 a)
  D1A :: (b -> f0 a) -> Definition (f1 b -> f0 a)
  D2  :: (c -> b -> a) -> Definition (f2 c -> f1 b -> f0 a)
  D2A :: (c -> b -> f0 a) -> Definition (f2 c -> f1 b -> f0 a)
  D3  :: (d -> c -> b -> a) -> Definition (f3 d -> f2 c -> f1 b -> f0 a)
  D3A :: (d -> c -> b -> f0 a) -> Definition (f3 d -> f2 c -> f1 b -> f0 a)

{- | Define (and @'DefineA@) provide a means to create new @'Definition'@s via 
@'define'@ (@'defineA'@). The @'define'@ function takes a single function input 
and returns a lifted function. For example,

@
f :: Int -> String -> Bool
f i s 
  | 1 "yes" = True
  | otherwise = FALSE

myFeature :: Definition (Feature "A" Int -> Feature "B" String -> Feature "C" Bool )
myFeature = define f
@

The @'defineA'@ function is similar, except that the return type of the input
function is already lifted. In the example below, an input of @Nothing@ is 
considered a missing state: 

@
f :: Int -> Maybe String -> Feature "C" Bool
f i s 
  | 1 (Just "yes")   = pure True
  | _ (Just _ )      = pure False -- False for any Int and any (Just String)
  | otherwise        = pure $ missingBecause InsufficientData -- missing if no string

myFeature :: Definition (Feature "A" Int -> Feature "B" String -> Feature "C" Bool )
myFeature = defineA f
@

-}
class Define inputs def | def -> inputs where
  define :: inputs -> Definition def

-- | See @'Define'@.
class DefineA inputs def | def -> inputs where
  defineA :: inputs -> Definition def

instance Define (b -> a) (FeatureData b -> FeatureData a) where define = D1
instance Define (c -> b -> a) (FeatureData c -> FeatureData b -> FeatureData a) where define = D2
instance Define (d -> c -> b -> a) (FeatureData d -> FeatureData c -> FeatureData b -> FeatureData a) where define = D3

instance DefineA (b -> FeatureData a) (FeatureData b -> FeatureData a) where defineA = D1A
instance DefineA (c -> b -> FeatureData a) (FeatureData c -> FeatureData b -> FeatureData a) where defineA = D2A
instance DefineA (d -> c -> b -> FeatureData a) (FeatureData d -> FeatureData c -> FeatureData b -> FeatureData a) where defineA = D3A

instance Define (b -> a) (Feature n1 b -> Feature n0 a) where define = D1
instance Define (c -> b -> a) (Feature n2 c -> Feature n1 b -> Feature n0 a) where define = D2
instance Define (d -> c -> b -> a) (Feature n3 d -> Feature n2 c -> Feature n1 b -> Feature n0 a) where define = D3

instance DefineA (b -> Feature n0 a) (Feature n1 b -> Feature n0 a) where defineA = D1A
instance DefineA (c -> b -> Feature n0 a) (Feature n2 c -> Feature n1 b -> Feature n0 a) where defineA = D2A
instance DefineA (d -> c -> b -> Feature n0 a) (Feature n3 d -> Feature n2 c -> Feature n1 b -> Feature n0 a) where defineA = D3A

{- | Evaluate a @Definition@. Note that (currently), the second argument of 'eval'
is a *tuple* of inputs. For example,

@
f :: Int -> String -> Bool
f i s 
  | 1 "yes" = True
  | otherwise = FALSE

myFeature :: Definition (Feature "A" Int -> Feature "B" String -> Feature "C" Bool )
myFeature = define f

a :: Feature "A" Int
a = pure 1

b :: Feature "B" String
b = pure "yes"

c = eval myFeature (a, b)
@

-}
class Eval def args return | def -> args return where
  eval :: Definition def -- ^ a @'Definition'@
                     -> args -- ^ a tuple of arguments to the @'Definition'@
                     -> return

instance Eval (FeatureData b -> FeatureData a) 
              (FeatureData b)  (FeatureData a) where
  eval (D1 f)  x = fmap f x
  eval (D1A f) x = x >>= f

instance Eval (Feature n1 b -> Feature n0 a)
              (Feature n1 b)  (Feature n0 a) where
  eval (D1 f) (MkFeature x) = MkFeature $ fmap f x
  eval (D1A f) (MkFeature x) =
       case fmap f x of
          MkFeatureData (Left l)  -> MkFeature $ MkFeatureData (Left l)
          MkFeatureData (Right r) -> r


instance Eval (FeatureData c -> FeatureData b -> FeatureData a) 
              (FeatureData c,   FeatureData b) (FeatureData a) where
  eval (D2 f) (x, y) = liftA2 f x y
  eval (D2A f) (x, y) = join (liftA2 f x y)

instance Eval (Feature n2 c -> Feature n1 b -> Feature n0 a)
              (Feature n2 c,   Feature n1 b)  (Feature n0 a)
  where
  eval (D2 f) (MkFeature x, MkFeature y) = MkFeature $ liftA2 f x y
  eval (D2A f) (MkFeature x, MkFeature y) =
      case liftA2 f x y of
          MkFeatureData (Left l)  -> MkFeature $ MkFeatureData (Left l)
          MkFeatureData (Right r) ->  r

instance Eval (FeatureData d -> FeatureData c -> FeatureData b -> FeatureData a)
              (FeatureData d,   FeatureData c,   FeatureData b)  (FeatureData a)
  where
  eval (D3 f) (x, y, z) = liftA3 f x y z
  eval (D3A f) (x, y, z) = join (liftA3 f x y z)

instance Eval (Feature n3 d -> Feature n2 c -> Feature n1 b -> Feature n0 a)
              (Feature n3 d,   Feature n2 c,   Feature n1 b)  (Feature n0 a)
   where
  eval (D3 f) (MkFeature x, MkFeature y, MkFeature z) = MkFeature $ liftA3 f x y z
  eval (D3A f) (MkFeature x, MkFeature y, MkFeature z) =
      case liftA3 f x y z of
          MkFeatureData (Left l)  -> MkFeature $ MkFeatureData (Left l)
          MkFeatureData (Right r) -> r

{- | Initializes @Feature@ @Attributes@ to empty strings -}

class HasAttributes n a where
  getAttributes :: Feature n a -> Attributes

-- instance HasAttributes name d where
--   getAttributes x = MkAttributes "" "" ""