-- | -- Module: Optics.Label -- Description: Overloaded labels as optics -- -- Overloaded labels are a solution to Haskell's namespace problem for records. -- The @-XOverloadedLabels@ extension allows a new expression syntax for labels, -- a prefix @#@ sign followed by an identifier, e.g. @#foo@. These expressions -- can then be given an interpretation that depends on the type at which they -- are used and the text of the label. -- -- The following example shows how overloaded labels can be used as optics. -- -- == Example -- -- Consider the following: -- -- >>> :set -XDataKinds -- >>> :set -XFlexibleContexts -- >>> :set -XFlexibleInstances -- >>> :set -XMultiParamTypeClasses -- >>> :set -XOverloadedLabels -- >>> :set -XTypeFamilies -- >>> :set -XUndecidableInstances -- >>> :{ -- data Human = Human -- { humanName :: String -- , humanAge :: Integer -- , humanPets :: [Pet] -- } deriving Show -- data Pet -- = Cat { petName :: String, petAge :: Int, petLazy :: Bool } -- | Fish { petName :: String, petAge :: Int } -- deriving Show -- :} -- -- The following instances can be generated by @makeFieldLabels@ from -- @Optics.TH@ in the @optics-th@ package: -- -- >>> :{ -- instance (k ~ A_Lens, a ~ String, b ~ String) => LabelOptic "name" k Human Human a b where -- labelOptic = lensVL $ \f s -> (\v -> s { humanName = v }) <$> f (humanName s) -- instance (k ~ A_Lens, a ~ Integer, b ~ Integer) => LabelOptic "age" k Human Human a b where -- labelOptic = lensVL $ \f s -> (\v -> s { humanAge = v }) <$> f (humanAge s) -- instance (k ~ A_Lens, a ~ [Pet], b ~ [Pet]) => LabelOptic "pets" k Human Human a b where -- labelOptic = lensVL $ \f s -> (\v -> s { humanPets = v }) <$> f (humanPets s) -- instance (k ~ A_Lens, a ~ String, b ~ String) => LabelOptic "name" k Pet Pet a b where -- labelOptic = lensVL $ \f s -> (\v -> s { petName = v }) <$> f (petName s) -- instance (k ~ A_Lens, a ~ Int, b ~ Int) => LabelOptic "age" k Pet Pet a b where -- labelOptic = lensVL $ \f s -> (\v -> s { petAge = v }) <$> f (petAge s) -- instance (k ~ An_AffineTraversal, a ~ Bool, b ~ Bool) => LabelOptic "lazy" k Pet Pet a b where -- labelOptic = atraversalVL $ \point f s -> case s of -- Cat name age lazy -> (\lazy' -> Cat name age lazy') <$> f lazy -- _ -> point s -- :} -- -- Here is some test data: -- -- >>> :{ -- peter :: Human -- peter = Human "Peter" 13 [ Fish "Goldie" 1 -- , Cat "Loopy" 3 False -- , Cat "Sparky" 2 True -- ] -- :} -- -- Now we can ask for Peter's name: -- -- >>> view #name peter -- "Peter" -- -- or for names of his pets: -- -- >>> toListOf (#pets % folded % #name) peter -- ["Goldie","Loopy","Sparky"] -- -- We can check whether any of his pets is lazy: -- -- >>> orOf (#pets % folded % #lazy) peter -- True -- -- or how things might be be a year from now: -- -- >>> peter & over #age (+1) & over (#pets % mapped % #age) (+1) -- Human {humanName = "Peter", humanAge = 14, humanPets = [Fish {petName = "Goldie", petAge = 2},Cat {petName = "Loopy", petAge = 4, petLazy = False},Cat {petName = "Sparky", petAge = 3, petLazy = True}]} -- -- Perhaps Peter is going on vacation and needs to leave his pets at home: -- -- >>> peter & set #pets [] -- Human {humanName = "Peter", humanAge = 13, humanPets = []} -- -- -- == Structure of 'LabelOptic' instances -- -- You might wonder why instances above are written in form -- -- @ -- instance (k ~ A_Lens, a ~ [Pet], b ~ [Pet]) => LabelOptic "pets" k Human Human a b where -- @ -- -- instead of -- -- @ -- instance LabelOptic "pets" A_Lens Human Human [Pet] [Pet] where -- @ -- -- The reason is that using the first form ensures that it is enough for GHC to -- match on the instance if either @s@ or @t@ is known (as type equalities are -- verified after the instance matches), which not only makes type inference -- better, but also allows it to generate better error messages. -- -- For example, if you try to write @peter & set #pets []@ with the appropriate -- LabelOptic instance in the second form, you get the following: -- -- @ -- <interactive>:16:1: error: -- • No instance for LabelOptic "pets" ‘A_Lens’ ‘Human’ ‘()’ ‘[Pet]’ ‘[a0]’ -- (maybe you forgot to define it or misspelled a name?) -- • In the first argument of ‘print’, namely ‘it’ -- In a stmt of an interactive GHCi command: print it -- @ -- -- That's because empty list doesn't have type @[Pet]@, it has type @[r]@ and -- GHC doesn't have enough information to match on the instance we -- provided. We'd need to either annotate the list: @peter & set #pets -- ([]::[Pet])@ or the result type: @peter & set #pets [] :: Human@, which is -- suboptimal. -- -- Here are more examples of confusing error messages if the instance for -- @LabelOptic "age"@ is written without type equalities: -- -- @ -- λ> view #age peter :: Char -- -- <interactive>:28:6: error: -- • No instance for LabelOptic "age" ‘k0’ ‘Human’ ‘Human’ ‘Char’ ‘Char’ -- (maybe you forgot to define it or misspelled a name?) -- • In the first argument of ‘view’, namely ‘#age’ -- In the expression: view #age peter :: Char -- In an equation for ‘it’: it = view #age peter :: Char -- λ> peter & set #age "hi" -- -- <interactive>:29:1: error: -- • No instance for LabelOptic "age" ‘k’ ‘Human’ ‘b’ ‘a’ ‘[Char]’ -- (maybe you forgot to define it or misspelled a name?) -- • When checking the inferred type -- it :: forall k b a. ((TypeError ...), Is k A_Setter) => b -- -- λ> age = #age :: Iso' Human Int -- -- <interactive>:7:7: error: -- • No instance for LabelOptic "age" ‘An_Iso’ ‘Human’ ‘Human’ ‘Int’ ‘Int’ -- (maybe you forgot to define it or misspelled a name?) -- • In the expression: #age :: Iso' Human Int -- In an equation for ‘age’: age = #age :: Iso' Human Int -- @ -- -- If we use the first form, error messages become more accurate: -- -- @ -- λ> view #age peter :: Char -- <interactive>:31:6: error: -- • Couldn't match type ‘Char’ with ‘Integer’ -- arising from the overloaded label ‘#age’ -- • In the first argument of ‘view’, namely ‘#age’ -- In the expression: view #age peter :: Char -- In an equation for ‘it’: it = view #age peter :: Char -- λ> peter & set #age "hi" -- -- <interactive>:32:13: error: -- • Couldn't match type ‘[Char]’ with ‘Integer’ -- arising from the overloaded label ‘#age’ -- • In the first argument of ‘set’, namely ‘#age’ -- In the second argument of ‘(&)’, namely ‘set #age "hi"’ -- In the expression: peter & set #age "hi" -- λ> age = #age :: Iso' Human Int -- -- <interactive>:9:7: error: -- • Couldn't match type ‘An_Iso’ with ‘A_Lens’ -- arising from the overloaded label ‘#age’ -- • In the expression: #age :: Iso' Human Int -- In an equation for ‘age’: age = #age :: Iso' Human Int -- @ -- -- == Limitations arising from functional dependencies -- -- Functional dependencies guarantee good type inference, but also -- create limitations. We can split them into two groups: -- -- - @name s -> k a@, @name t -> k b@ -- -- - @name s b -> t@, @name t a -> s@ -- -- The first group ensures that when we compose two optics, the middle type is -- unambiguous. The consequence is that it's not possible to create label optics -- with @a@ or @b@ referencing type variables not referenced in @s@ or @t@, -- i.e. getters for fields of rank 2 type or reviews for constructors with -- existentially quantified types inside. -- -- The second group ensures that when we perform a chain of updates, the middle -- type is unambiguous. The consequence is that it's not possible to define -- label optics that: -- -- - Modify phantom type parameters of type @s@ or @t@. -- -- - Modify type parameters of type @s@ or @t@ if @a@ or @b@ contain ambiguous -- applications of type families to these type parameters. -- module Optics.Label ( LabelOptic(..) , LabelOptic' ) where import Optics.Internal.Optic -- $setup -- >>> import Optics.Core