packages feed

generic-labels-0.1.0.1: src/Data/Generic/Labels.hs

{-# LANGUAGE UndecidableInstances #-}

{-|
Module: Data.Generic.Labels
Description: Convert between collections of fields using generics.

This module provides functionality for converting between different collections of fields.

== Projections

To project out certain fields of a record, use 'project'.

For instance:

@
data IBXD x = IBXD { i :: Int, b :: Bool, x :: x, d :: Double }
  deriving stock Generic
data XI x = XI { x :: c, i :: Int }
  deriving stock Generic
@

We can project out the two fields of interest from the first record type:

@
ibxd_to_xi :: IBXD x -> XI x
ibxd_to_xi = project
@

== Injections

Going the other way, we can use 'inject' to override fields of the larger record
with those from the smaller record:

@
xi_into_ibxd :: XI x -> IBXD x -> IBXD x
xi_into_ibxd = inject
@

== Adapters

'project' and 'inject' are two instances of the more general 'adapt' function,
which allows us to only specify the missing arguments in the above example.

@
xi_plus_bd_makes_ibxd :: XI x -> ( "b" := Bool, "d" := Double ) -> IBXD x
xi_plus_bd_makes_ibxd = adapt
@

In this situation, we are building up a record of type @IBXD x@ out of two parts.

More generally, `adapt` allows for fields in the first argument to override
fields in the second argument, which provides a convenient mechanism for
named optional arguments.

@
adapt :: _ => givenArgs -> optionalArgs -> allArgs
@

For instance, if we have a function @f@ which takes in several named arguments

@
type AllArgsTuple = ( "arg1" := Ty1, "arg2" := Ty2, "arg3" := Ty3, "arg4" := Ty4 ) 

f :: AllArgsTuple -> r
@

and we have default values for some of those arguments, e.g.

@
type DefaultArgsTuple = ( "arg2" := Ty2, "arg3" := Ty3 )

f_defaults :: DefaultArgsTuple
f_defaults = ( #arg2 := val2, #arg3 := val3 )
@

then we can create a corresponding function @f_defaulting@,
which allows user to only pass the remaining (required) arguments:

@
f_defaulting
  :: CheckedAdapt args DefaultArgsTuple AllArgsTuple => args -> r
f_defaulting args = adapt args f_defaults
@

-}

module Data.Generic.Labels
  (
  -- * Converting between collections of fields
    Adapt(..), Inject(..), Project(..)

  -- * Re-export of labelling functionality from "Data.Label"
  , (:=)(..)

  -- * Unchecked functions (can behave unpredictably).
  , UncheckedAdapt(..), UncheckedInject(..), UncheckedProject(..)
  )
  where

-- base
import GHC.Generics

-- generic-labels
import Data.Label
  ( (:=)(..) )
import Data.Generic.Labels.Internal
  ( GAdapt
    ( gAdapt )
  )
import Data.Generic.Labels.Internal.Errors
  ( CheckAdapt, CheckInject, CheckProject )

--------------------------------------------------------------------------------

class UncheckedAdapt args opt all where
  -- | Create an adapter, without extra type-level validation.
  --
  -- Unchecked uses (e.g. presence of duplicate fields)
  -- can throw abstruse compile-time error messages
  -- or produce unexpected results at runtime.
  --
  -- Prefer using 'adapt' whenever possible.
  uncheckedAdapt :: args -> opt -> all

instance {-# OVERLAPPING #-} UncheckedAdapt a a a where
  uncheckedAdapt = const
instance {-# OVERLAPPING #-} UncheckedAdapt a opt a where
  uncheckedAdapt = const
instance {-# OVERLAPPING #-} ( a ~ b )
       => UncheckedAdapt ( lbl := a ) opt ( lbl := b ) where
  uncheckedAdapt = const
instance {-# OVERLAPPING #-} ( a ~ b, o ~ b )
       => UncheckedAdapt ( lbl := a ) ( lbl := o ) ( lbl := b ) where
  uncheckedAdapt = const

instance {-# OVERLAPPING #-}
    ( Generic all
    , argFld ~ S1 ( MetaSel ( Just lbl1 ) NoSourceUnpackedness NoSourceStrictness DecidedLazy ) ( Rec0 a )
    , optFld ~ S1 ( MetaSel ( Just lbl2 ) NoSourceUnpackedness NoSourceStrictness DecidedLazy ) ( Rec0 o )
    , GAdapt argFld optFld ( Rep all )
    )
  => UncheckedAdapt ( lbl1 := a ) ( lbl2 := o ) all where
  uncheckedAdapt ( _ := arg ) ( _ := opt ) =
    to $ gAdapt ( M1 ( K1 arg ) :: argFld x ) ( M1 ( K1 opt ) :: optFld x )

instance
    ( Generic opt, Generic all
    , argFld ~ S1 ( MetaSel ( Just lbl ) NoSourceUnpackedness NoSourceStrictness DecidedLazy ) ( Rec0 a )
    , GAdapt argFld ( Rep opt ) ( Rep all )
    )
  => UncheckedAdapt ( lbl := a ) opt all where
  uncheckedAdapt ( _ := arg ) opt =
    to $ gAdapt ( M1 ( K1 arg ) :: argFld x ) ( from opt )

instance {-# OVERLAPPING #-}
    ( Generic args, Generic all
    , optFld ~ S1 ( MetaSel ( Just lbl ) NoSourceUnpackedness NoSourceStrictness DecidedLazy ) ( Rec0 o )
    , GAdapt ( Rep args ) optFld ( Rep all )
    )
  => UncheckedAdapt args ( lbl := o ) all where
  uncheckedAdapt args ( _ := opt ) =
    to $ gAdapt ( from args ) ( M1 ( K1 opt ) :: optFld x )

instance {-# OVERLAPPABLE #-}
    ( Generic args, Generic opt, Generic all
    , GAdapt ( Rep args ) ( Rep opt ) ( Rep all )
    )
  => UncheckedAdapt args opt all where
  uncheckedAdapt args opt =
    to $ gAdapt ( from args ) ( from opt )

class    ( UncheckedAdapt args opt all ) => Adapt args opt all where
  -- | Create an adapter, to inject a smaller type into a larger one,
  -- providing defaults for optional values.
  --
  -- @
  --   myAdapt :: ( "i" := Int, "f" := Float ) -> ( "f" := Float, "b" := Bool, "i" := Int )
  --   myAdapt args = adapt args ( #b := False )
  -- @
  --
  -- @
  --   > myAdapt ( #i := 3, #f := 17.1 )
  --   > ( #f = 17.1, #b = False, #i := 3 )
  -- @
  --
  -- Here @ myAdapt @ re-arranges the arguments into the result,
  -- passing in additional (default) values that are overriden
  -- when they occur in the arguments.
  --
  -- Includes custom validation, e.g. to disallow duplicate arguments.
  -- Use 'uncheckedAdapt' to disable this validation
  -- (you might get strange errors!).
  adapt
    :: args -- ^ Provided arguments
    -> opt  -- ^ Default values of optional arguments
    -> all  -- ^ Combination of provided arguments and non-overridden defaults

instance ( UncheckedAdapt args opt all, CheckAdapt args opt all )
      => Adapt args opt all where
  adapt = uncheckedAdapt

class    UncheckedAdapt small big big => UncheckedInject small big where
  -- | Inject a smaller type into a larger one, without extra type-level validation.
  --
  -- Unchecked uses (e.g. presence of duplicate fields)
  -- can throw abstruse compile-time error messages
  -- or produce unexpected results at runtime.
  --
  -- Prefer using 'inject' whenever possible.
  uncheckedInject :: small -> big -> big

instance UncheckedAdapt small big big => UncheckedInject small big where
  uncheckedInject = uncheckedAdapt

class    ( UncheckedInject small big ) => Inject small big where
  -- | Inject a smaller type into a larger one,
  -- overriding the fields in the larger type with those from the smaller type.
  --
  -- @
  --   myInject
  --     :: ( "i" := Int, "f" := Float )
  --     -> ( "f" := Float, "b" := Bool, "i" := Int )
  --     -> ( "f" := Float, "b" := Bool, "i" := Int )
  --   myInject = inject
  -- @
  --
  -- Here @ myInject @ overrides the fields of the second argument
  -- with those provided in the first argument.
  --
  -- @
  --   > myInject ( #i := 3, #f := 17.1 ) ( #f := 9.0, #b := False, #i := 22 )
  --   > ( #f := 17.1, #b := False, #i := 3 )
  -- @
  --
  -- Includes custom validation, e.g. to disallow duplicate arguments.
  -- Use 'uncheckedInject' to disable this validation
  -- (you might get strange errors!).
  inject :: small -> big -> big

instance ( UncheckedInject small big, CheckInject small big )
      => Inject small big where
  inject = uncheckedInject

class    UncheckedAdapt big big small => UncheckedProject big small where
  -- | Project a smaller type out from a larger one, without extra type-level validation.
  --
  -- Unchecked uses (e.g. presence of duplicate fields)
  -- can throw abstruse compile-time error messages
  -- or produce unexpected results at runtime.
  --
  -- Prefer using 'project' whenever possible.
  uncheckedProject :: big -> small

instance UncheckedAdapt big big small => UncheckedProject big small where
  uncheckedProject big = uncheckedAdapt big big

class    ( UncheckedProject big small ) => Project big small where
  -- | Project a smaller type out from a larger one, discarding the rest.
  --
  -- @
  --   myProject :: ( "f" := Float, "b" := Bool, "i" := Int ) -> ( "i" := Int, "f" := Float )
  --   myProject = project
  -- @
  --
  -- Here @ myProject @ projects out a sub-component of the whole type,
  -- in this case discarding the boolean while re-arranging the other fields.
  --
  -- @
  --   > myProject ( #f := 17.1, #b := False, #i := 3 )
  --   > ( #i := 3, #f := 17.1 )
  -- @
  --
  -- Includes custom validation, e.g. to disallow duplicate arguments.
  -- Use 'uncheckedProject' to disable this validation
  -- (you might get strange errors!).
  project :: big -> small

instance ( UncheckedProject big small, CheckProject big small )
      => Project big small where
  project = uncheckedProject