{-| This library provide a brief implementation for extensible records.
It is sensitive to the ordering of key-value items, but has simple type constraints and provides short compile time.
-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE ApplicativeDo #-}
module Data.KVList
(
-- * Constructors
-- $setup
KVList
, (:=)((:=))
, (&=)
, (&=>)
, kvcons
, empty
, singleton
, ListKey(..)
-- * Operators
, get
, HasKey
, (&.)
)
where
import Prelude
import Data.Functor ((<&>))
import qualified Data.List as List
import Data.Kind (Constraint, Type)
import Data.Typeable (Typeable, typeOf)
import GHC.TypeLits (KnownSymbol, Symbol, TypeError, ErrorMessage(Text))
import GHC.OverloadedLabels (IsLabel(..))
import Unsafe.Coerce (unsafeCoerce)
-- Constructors
{- $setup #constructors#
We can create type level KV list as follows.
>>> :set -XOverloadedLabels -XTypeOperators
>>> import Prelude
>>> import Data.KVList (empty, KVList, (:=)((:=)), (&.), (&=))
>>> let sampleList = empty &= #foo := "str" &= #bar := 34
>>> type SampleList = KVList '[ "foo" := String, "bar" := Int ]
-}
{-| A value with type level key.
-}
data KVList (kvs :: [Type]) where
KVNil :: KVList '[]
KVCons :: (KnownSymbol key) => key := v -> KVList xs -> KVList ((key := v) ': xs)
instance Eq (KVList '[]) where
(==) _ _ = True
instance (Eq v, Eq (KVList kvs)) => Eq (KVList ((k := v) ': kvs) ) where
(==) (KVCons (_ := v1) next1) (KVCons (_ := v2) next2) = v1 == v2 && next1 == next2
{-| -}
instance ShowFields (KVList kvs) => Show (KVList kvs) where
show kvs =
List.unlines $
"KVList.empty" : showFields kvs
class ShowFields a where
showFields :: a -> [String]
instance ShowFields (KVList '[]) where
showFields _ = []
instance ( ShowFields (KVList kvs)
, Show v
) => ShowFields (KVList ((k := v) ': kvs)) where
showFields (KVCons (k := v) next) =
let
firstLine str =
List.unwords
[ "&="
, "#" <> show k
, ":="
, str
]
in
( case List.lines $ show v of
[] -> [ firstLine "" ]
[a] -> [ firstLine a ]
as ->
List.concat
[ [ firstLine "(" ]
, as <&> \x -> " " ++ x
, [ ")" ]
]
) ++ showFields next
{-| -}
empty :: KVList '[]
empty = KVNil
{-| -}
(&=) :: (KnownSymbol k, Appended kvs '[k := v] ~ appended) => KVList kvs -> (k := v) -> KVList appended
(&=) kvs kv = append kvs (singleton kv)
{-# INLINE (&=) #-}
infixl 1 &=
{-| Applicative version of '(&=)'.
-
- >>> pure KVList.empty
- >>> &=> #foo := (Just 3)
- >>> &=> #bar := (Just "bar")
- Just $ KVList.empty &= #foo := 3 &= #bar := "bar"
-
- >>> pure KVList.empty
- >>> &=> #foo := (Just 3)
- >>> &=> #bar := Nothing
- Nothing
-}
(&=>) :: (Applicative f, KnownSymbol k, Appended kvs '[k := v] ~ appended) => f (KVList kvs) -> (k := f v) -> f (KVList appended)
(&=>) fkvs (k := fv) = do
kvs <- fkvs
v <- fv
pure $ (&=) kvs (k := v)
{-# INLINE (&=>) #-}
infixl 1 &=>
{-| -}
kvcons :: (KnownSymbol k) => (k := v) -> KVList kvs -> KVList ((k := v) ': kvs)
kvcons = KVCons
{-| -}
data (key :: Symbol) := (value :: Type) where
(:=) :: ListKey a -> b -> a := b
infix 2 :=
deriving instance (Show value) => Show (key := value)
{-| -}
type HasKey (key :: Symbol) (kvs :: [Type]) (v :: Type) = HasKey_ key kvs kvs v
type family HasKey_ (key :: Symbol) (kvs :: [Type]) (orig :: [Type]) (v :: Type) :: Constraint where
HasKey_ key '[] '[] v = TypeError ('Text "The KVList is empty.")
HasKey_ key '[] orig v = TypeError ('Text "The Key is not in the KVList.")
HasKey_ key ((key := val) ': _) _ v = (val ~ v)
HasKey_ key (_ ': kvs) orig v = HasKey_ key kvs orig v
{-| -}
type family Appended kvs1 kv2 :: [Type] where
Appended '[] kv2 = kv2
Appended (kv ': kvs) kv2 =
kv ': Appended kvs kv2
{-| -}
append :: (Appended kvs1 kvs2 ~ appended) => KVList kvs1 -> KVList kvs2 -> KVList appended
append KVNil kvs2 = kvs2
append (KVCons kv kvs) kvs2 = KVCons kv (append kvs kvs2)
{-| -}
singleton :: (KnownSymbol k) => (k := v) -> KVList '[ k := v ]
singleton kv = KVCons kv KVNil
{-| -}
get :: (KnownSymbol key, HasKey key kvs v) => ListKey key -> KVList kvs -> v
get p kvs = get_ p kvs kvs
get_ :: (KnownSymbol key, HasKey key orig v) => ListKey key -> KVList kvs -> KVList orig -> v
get_ _ KVNil KVNil = error "Unreachable: The KVList is empty."
get_ _ KVNil _ = error "Unreachable: The Key is not in the KVList."
get_ p (KVCons (k := v) kvs) orig =
if typeOf p == typeOf k then
unsafeCoerce v
else
get_ p kvs orig
{-| -}
(&.) :: (KnownSymbol key, HasKey key kvs v) => KVList kvs -> ListKey key -> v
(&.) kvs k = get k kvs
infixl 9 &.
{-| 'ListKey' is just a proxy, but needed to implement a non-orphan 'IsLabel' instance.
In most cases, you only need to create a `ListKey` instance with @OverloadedLabels@, such as `#foo`.
-}
data ListKey (t :: Symbol)
= ListKey
deriving (Show, Eq, Typeable)
instance l ~ l' => IsLabel (l :: Symbol) (ListKey l') where
#if MIN_VERSION_base(4, 10, 0)
fromLabel = ListKey
#else
fromLabel _ = ListKey
#endif