Last active
May 19, 2023 11:12
-
-
Save Profpatsch/c1992885fd28294968c549e2237ced3f to your computer and use it in GitHub Desktop.
Labelled Tuples/Enums in GHC >9.2 Haskell (Hackage: https://hackage.haskell.org/package/pa-label)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{-# LANGUAGE DataKinds #-} | |
{-# LANGUAGE DerivingStrategies #-} | |
{-# LANGUAGE LambdaCase #-} | |
module Label | |
( -- * Labels | |
Label, | |
label, | |
label', | |
getLabel, | |
mapLabel, | |
traverseLabel, | |
-- * Named Tuples | |
T2 (..), | |
focusOnField, | |
monoMapT2, | |
T3 (..), | |
monoMapT3, | |
-- * Named Sums/Enums | |
E2 (..), | |
mapE2, | |
monoMapE2, | |
monoFoldE2, | |
monoTraverseE2, | |
partitionE2, | |
E3 (..), | |
mapE3, | |
) | |
where | |
import Data.Data (Proxy (..)) | |
import Data.Either (partitionEithers) | |
import Data.Function ((&)) | |
import Data.Functor ((<&>)) | |
import Data.Typeable (Typeable) | |
import GHC.Records (HasField (..)) | |
import GHC.TypeLits (KnownSymbol, Symbol, symbolVal) | |
-- | A labelled value. | |
-- | |
-- Use 'label'/'label'' to construct, | |
-- then use dot-syntax to get the inner value. | |
newtype Label (label :: Symbol) value = Label value | |
deriving stock (Eq, Ord) | |
deriving newtype (Typeable) | |
instance (KnownSymbol label, Show value) => Show (Label label value) where | |
showsPrec d (Label val) = | |
showParen (d > 10) $ | |
showString "label @" | |
. showsPrec 11 (symbolVal (Proxy @label)) | |
. showString " " | |
. showsPrec 11 val | |
-- | Attach a label to a value; should be used with a type application to name the label. | |
-- | |
-- @ | |
-- let f = label @"foo" 'f' :: Label "foo" Char | |
-- in f.foo :: Char | |
-- @ | |
-- | |
-- Use dot-syntax to get the labelled value. | |
label :: forall label value. value -> Label label value | |
label value = Label value | |
-- | Attach a label to a value; Pass it a proxy with the label name in the argument type. | |
-- This is intended for passing through the label value; | |
-- you can also use 'label'. | |
-- | |
-- | |
-- @ | |
-- let f = label' (Proxy @"foo") 'f' :: Label "foo" Char | |
-- in f.foo :: Char | |
-- @ | |
-- | |
-- Use dot-syntax to get the labelled value. | |
label' :: forall label value. (Proxy label) -> value -> Label label value | |
label' Proxy value = Label value | |
-- | Fetches the labelled value. | |
instance HasField label (Label label value) value where | |
getField :: (Label label value) -> value | |
getField (Label value) = value | |
-- | Fetch a value from a record, like 'getField', but also keep it wrapped by its label. | |
getLabel :: forall label record a. HasField label record a => record -> Label label a | |
getLabel rec = rec & getField @label & label @label | |
-- | 'fmap' over the contents of the labbelled value. Helper. | |
mapLabel :: forall label a b. (a -> b) -> Label label a -> Label label b | |
mapLabel f (Label a) = Label @label $ f a | |
-- | 'traverse' over the contents of the labbelled value. Helper. | |
traverseLabel :: forall label f a b. Functor f => (a -> f b) -> Label label a -> f (Label label b) | |
traverseLabel fab (Label a) = Label @label <$> fab a | |
-- | A named 2-element tuple. Since the elements are named, you can access them with `.`. | |
-- | |
-- @ | |
-- let t2 = T2 (label @"myfield" 'c') (label @"otherfield" True) :: T2 "myfield" Char "otherfield" Bool | |
-- in ( | |
-- t2.myfield :: Char, | |
-- t2.otherfield :: Bool | |
-- ) | |
-- @ | |
data T2 (l1 :: Symbol) t1 (l2 :: Symbol) t2 = T2 (Label l1 t1) (Label l2 t2) | |
deriving stock (Show, Eq) | |
-- | Access the first field by label | |
instance HasField l1 (T2 l1 t1 l2 t2) t1 where | |
getField (T2 t1 _) = getField @l1 t1 | |
-- | Access the second field by label | |
instance HasField l2 (T2 l1 t1 l2 t2) t2 where | |
getField (T2 _ t2) = getField @l2 t2 | |
-- | Given a record with some field, “focus” on that field by pulling it into the first part of the T2, | |
-- and put the original record into the second part of the T2. | |
-- | |
-- This can be useful when you have a function that requires something with a field, | |
-- but the field itself is nested somewhere in the record. | |
-- | |
-- Example: | |
-- | |
-- @ | |
-- data Foo = Foo | |
-- { nested :: Label "myId" Text | |
-- } | |
-- | |
-- foo = Foo {nested = "hi"} | |
-- | |
-- fn :: HasField "myId" rec Text => rec -> Text | |
-- fn rec = rec.myId <> "!" | |
-- | |
-- x = fn (focusOnField @"myId" (.nested) foo) == "hi!" | |
-- @ | |
-- | |
-- Note that you will have to give `focusOnField` a type annotation of which label to use, | |
-- otherwise it cannot infer it. | |
focusOnField :: | |
forall field rec subrec t. | |
HasField field subrec t => | |
(rec -> subrec) -> | |
rec -> | |
T2 field t "dat" rec | |
focusOnField zoom rec = T2 (getLabel @field (rec & zoom)) (label @"dat" rec) | |
-- | Map a function over all fields in the tuple. All fields have to have the same type. | |
monoMapT2 :: (t -> t') -> T2 l1 t l2 t -> T2 l1 t' l2 t' | |
monoMapT2 f (T2 a b) = T2 (mapLabel f a) (mapLabel f b) | |
-- | A named 3-element tuple. Since the elements are named, you can access them with `.`. See 'T2' for an example. | |
data T3 (l1 :: Symbol) t1 (l2 :: Symbol) t2 (l3 :: Symbol) t3 = T3 (Label l1 t1) (Label l2 t2) (Label l3 t3) | |
deriving stock (Show, Eq) | |
-- | Access the first field by label | |
instance HasField l1 (T3 l1 t1 l2 t2 l3 t3) t1 where | |
getField (T3 t1 _ _) = getField @l1 t1 | |
-- | Access the second field by label | |
instance HasField l2 (T3 l1 t1 l2 t2 l3 t3) t2 where | |
getField (T3 _ t2 _) = getField @l2 t2 | |
-- | Access the third field by label | |
instance HasField l3 (T3 l1 t1 l2 t2 l3 t3) t3 where | |
getField (T3 _ _ t3) = getField @l3 t3 | |
-- | Map a function over all fields in the tuple. All fields have to have the same type. | |
monoMapT3 :: (t -> t') -> T3 l1 t l2 t l3 t -> T3 l1 t' l2 t' l3 t' | |
monoMapT3 f (T3 a b c) = T3 (mapLabel f a) (mapLabel f b) (mapLabel f c) | |
-- | A named 2-alternative sum (“'Either' with labels”). | |
data E2 (l1 :: Symbol) t1 (l2 :: Symbol) t2 | |
= E21 (Label l1 t1) | |
| E22 (Label l2 t2) | |
deriving (Eq, Show) | |
instance (Bounded t1, Bounded t2) => Bounded (E2 l1 t1 l2 t2) where | |
minBound = E21 (label @l1 minBound) | |
maxBound = E22 (label @l2 maxBound) | |
-- TODO: instance for arbitrary Enum types? | |
instance Enum (E2 l1 () l2 ()) where | |
toEnum 0 = E21 (label @l1 ()) | |
toEnum 1 = E22 (label @l2 ()) | |
toEnum _ = error "E2: toEnum" | |
fromEnum (E21 _) = 0 | |
fromEnum (E22 _) = 1 | |
-- | Map a separate function over every possibility in this enum. The label names stay the same. | |
-- | |
-- Each function has access to its label, this is intentional so that you have to mention the label once (e.g. by using dot-notation), to prevent confusing the cases. | |
mapE2 :: | |
forall l1 t1 t1' l2 t2 t2'. | |
(Label l1 t1 -> t1') -> | |
(Label l2 t2 -> t2') -> | |
E2 l1 t1 l2 t2 -> | |
E2 l1 t1' l2 t2' | |
mapE2 f1 f2 = \case | |
E21 lbl -> lbl & getLabel @l1 & f1 & label @l1 & E21 | |
E22 lbl -> lbl & getLabel @l2 & f2 & label @l2 & E22 | |
-- | Map a single function over every possiblity in this enum. All fields have to have the same type. | |
monoMapE2 :: (t -> t') -> E2 l1 t l2 t -> E2 l1 t' l2 t' | |
monoMapE2 f = \case | |
E21 lbl -> lbl & mapLabel f & E21 | |
E22 lbl -> lbl & mapLabel f & E22 | |
-- | If ever branch of this enum has the same type, fold the enum into its contents. | |
-- This loses the distinction between cases. | |
monoFoldE2 :: E2 l1 t l2 t -> t | |
monoFoldE2 = \case | |
E21 (Label t) -> t | |
E22 (Label t) -> t | |
-- | Partition a list of E2 into two lists that each keep their respective label. | |
-- Like 'partitionEithers', but with labels. | |
partitionE2 :: forall l1 t1 l2 t2. [E2 l1 t1 l2 t2] -> T2 l1 [t1] l2 [t2] | |
partitionE2 es = | |
es | |
<&> ( \case | |
E21 (Label t1) -> Left t1 | |
E22 (Label t2) -> Right t2 | |
) | |
& partitionEithers | |
& (\(t1s, t2s) -> T2 (label @l1 t1s) (label @l2 t2s)) | |
-- | Map a monadic (actually just a functor-ic) function over each possibility in this enum. All fields have to have the same type. | |
monoTraverseE2 :: Functor f => (t -> f t') -> E2 l1 t l2 t -> f (E2 l1 t' l2 t') | |
monoTraverseE2 f = \case | |
E21 lbl -> lbl & traverseLabel f <&> E21 | |
E22 lbl -> lbl & traverseLabel f <&> E22 | |
-- | A named 3-alternative sum (“'Either' with labels”). | |
data E3 (l1 :: Symbol) t1 (l2 :: Symbol) t2 (l3 :: Symbol) t3 | |
= E31 (Label l1 t1) | |
| E32 (Label l2 t2) | |
| E33 (Label l3 t3) | |
deriving (Eq, Show) | |
instance (Bounded t1, Bounded t3) => Bounded (E3 l1 t1 l2 t2 l3 t3) where | |
minBound = E31 (label @l1 minBound) | |
maxBound = E33 (label @l3 maxBound) | |
-- TODO: instance for arbitrary Enum types? | |
instance Enum (E3 l1 () l2 () l3 ()) where | |
toEnum 0 = E31 (label @l1 ()) | |
toEnum 1 = E32 (label @l2 ()) | |
toEnum 2 = E33 (label @l3 ()) | |
toEnum _ = error "E3: toEnum" | |
fromEnum (E31 _) = 0 | |
fromEnum (E32 _) = 1 | |
fromEnum (E33 _) = 2 | |
-- | Map a function over every element in this enum. The label names stay the same. | |
mapE3 :: | |
forall l1 t1 t1' l2 t2 t2' l3 t3 t3'. | |
(Label l1 t1 -> t1') -> | |
(Label l2 t2 -> t2') -> | |
(Label l3 t3 -> t3') -> | |
E3 l1 t1 l2 t2 l3 t3 -> | |
E3 l1 t1' l2 t2' l3 t3' | |
mapE3 f1 f2 f3 = \case | |
E31 lbl -> lbl & getLabel @l1 & f1 & label @l1 & E31 | |
E32 lbl -> lbl & getLabel @l2 & f2 & label @l2 & E32 | |
E33 lbl -> lbl & getLabel @l3 & f3 & label @l3 & E33 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment