Safe Haskell | None |
---|---|
Language | Haskell2010 |
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.
Synopsis
- class LabelOptic (name :: Symbol) k s t a b | name s -> k a, name t -> k b, name s b -> t, name t a -> s where
- labelOptic :: Optic k NoIx s t a b
- type LabelOptic' name k s a = LabelOptic name k s s a a
How to use labels as optics to make working with Haskell's records more convenient
The problem
Standard Haskell records are a common source of frustration amongst seasoned Haskell programmers. Their main issues are:
- Inability to define multiple data types sharing field names in the same module.
- Pollution of global namespace as every field accessor is also a top-level function.
- Clunky update syntax, especially when nested fields get involved.
Over the years multiple language extensions were proposed and implemented to alleviate these issues. We're quite close to having a reasonable solution with the following trifecta:
DuplicateRecordFields
- introduced in GHC 8.0.1, addresses (1)NoFieldSelectors
- accepted GHC proposal, addresses (2)RecordDotSyntax
- accepted GHC proposal, addresses (3)
It needs to be noted however that both NoFieldSelectors
and
RecordDotSyntax
are not yet implemented, with the latter depending on
adding setField
to HasField
(ghc/16232), not yet
merged.
Is there no hope then for people who would like to work with records in a reasonable way without waiting for these extensions? Not necessarily, as by following a couple of simple patterns we can get pretty much the same (and more) features with labels as optics, just with a slightly more verbose syntax.
The solution
Prefixless fields with DuplicateRecordFields
We necessarily want field names to be prefixless, i.e. field
to be a field
name and #field
to be an overloaded label that becomes an optic refering to
this field in the appropriate context. With this approach we get working
autocompletion and jump-to-definition in editors supporting ctags
/etags
in combination with hasktags
,
both of which (especially the latter) are very important for developer's
productivity in real-world code bases.
Let's look at data types defined with this approach in mind:
{-# LANGUAGE DuplicateRecordFields #-} import Data.Time data User = User { id :: Int , name :: String , joined :: UTCTime , movies :: [Movie] } data Movie = Movie { id :: Int , name :: String , releaseDate :: UTCTime }
Then appropriate LabelOptic
instances can be either written by hand or
generated using Template Haskell functions (defined in
Optics.TH
module from optics-th
package)
with
makeFieldLabelsWith noPrefixFieldLabels ''User makeFieldLabelsWith noPrefixFieldLabels ''Movie
Note: there exists a similar approach that involves prefixing field names
with the underscore and generation of lenses as ordinary functions so that
_field
is the ordinary field name and field
is the lens referencing
it. The drawback of such solution is inability to get working
jump-to-definition for field names, which makes navigation in unfamiliar code
bases significantly harder, so it's not recommended.
Emulation of NoFieldSelectors
Prefixless fields (especially ones with common names such as id
or name
)
leak into global namespace as accessor functions and can generate a lot of
name clashes. Before NoFieldSelectors
is available, this can be alleviated by
splitting modules defining types into two, namely:
- A private one that exports full type definitions, i.e. with their fields and constructors.
- A public one that exports only constructors (or no constructors at all if the data type in question is opaque).
There is no notion of private and public modules within a single cabal
target, but we can hint at it e.g. by naming the public module T
and
private T.Internal
.
An example:
Private module:
{-# LANGUAGE DataKinds #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE UndecidableInstances #-} module User.Internal (User(..)) where import Optics.TH data User = User { id :: Int , name :: String } makeFieldLabelsWith noPrefixFieldLabels ''User ...
Public module:
module User (User(User)) where import User.Internal ...
Then, whenever we're dealing with a value of type User
and want to read or
modify its fields, we can use corresponding labels without having to import
User.Internal
. Importing User
is enough because it provides appropriate
LabelOptic
instances through User.Internal
which enables labels to be
interpreted as optics in the appropriate context.
Note: if you plan to completely hide (some of) the fields of a data type,
you need to skip defining the corresponding LabelOptic
instances for them
(in case you want fields to be read only, you can make the optic kind of the
coresponding LabelOptic
A_Getter
instead of A_Lens
). It's because
Haskell makes it impossible to selectively hide instances, so once a
LabelOptic
instance is defined, it'll always be possible to use a label
that desugars to its usage whenever a module with its definition is
(transitively) imported.
{-# LANGUAGE OverloadedLabels #-} import Optics import User greetUser :: User -> String greetUser user = "Hello " ++ user ^. #name ++ "!" addSurname :: String -> User -> User addSurname surname user = user & #name %~ (++ " " ++ surname)
But what if we want to create a new User
with the record syntax? Importing
User
module is not sufficient since it doesn't export User
's
fields. However, if we import User.Internal
fully qualified and make use
of the fact that field names used within the record syntax don't have to be
prefixed when DisambiguateRecordFields
language extension is enabled, it
works out:
{-# LANGUAGE DisambiguateRecordFields #-} import User import qualified User.Internal newUser :: User newUser = User { id = 1 -- not User.Internal.id , name = "Ian" -- not User.Internal.name }
This way top-level field accessor functions stay in their own qualified namespace and don't generate name clashes, yet they can be used without prefix within the record syntax.
The result
When we follow the above conventions for data types in our application, we get:
- Prefixless field names that don't pollute global namespace (with the internal module qualification trick).
- Working tags based jump-to-definition for field names (as
field
is the ordinary field, whereas#field
is the lens referencing it). - The full power of optics at our disposal, should we ever need it.
Sample usage
An example showing how overloaded labels can be used as optics.
>>>
:set -XDataKinds
>>>
:set -XDuplicateRecordFields
>>>
:set -XFlexibleInstances
>>>
:set -XMultiParamTypeClasses
>>>
:set -XOverloadedLabels
>>>
:set -XTypeFamilies
>>>
:set -XUndecidableInstances
>>>
:{
data Human = Human { name :: String , age :: Integer , pets :: [Pet] } deriving Show data Pet = Cat { name :: String, age :: Int, lazy :: Bool } | Fish { name :: String, age :: Int } deriving Show :}
The following instances can be generated by makeFieldLabelsWith
noPrefixFieldLabels
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 (Human name age pets) -> (\name' -> Human name' age pets) <$> f name instance (k ~ A_Lens, a ~ Integer, b ~ Integer) => LabelOptic "age" k Human Human a b where labelOptic = lensVL $ \f (Human name age pets) -> (\age' -> Human name age' pets) <$> f age instance (k ~ A_Lens, a ~ [Pet], b ~ [Pet]) => LabelOptic "pets" k Human Human a b where labelOptic = lensVL $ \f (Human name age pets) -> (\pets' -> Human name age pets') <$> f pets instance (k ~ A_Lens, a ~ String, b ~ String) => LabelOptic "name" k Pet Pet a b where labelOptic = lensVL $ \f s -> case s of Cat name age lazy -> (\name' -> Cat name' age lazy) <$> f name Fish name age -> (\name' -> Fish name' age ) <$> f name instance (k ~ A_Lens, a ~ Int, b ~ Int) => LabelOptic "age" k Pet Pet a b where labelOptic = lensVL $ \f s -> case s of Cat name age lazy -> (\age' -> Cat name age' lazy) <$> f age Fish name age -> (\age' -> Fish name age' ) <$> f age 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 { name = "Peter" , age = 13 , pets = [ Fish { name = "Goldie" , age = 1 } , Cat { name = "Loopy" , age = 3 , lazy = False } , Cat { name = "Sparky" , age = 2 , lazy = 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 {name = "Peter", age = 14, pets = [Fish {name = "Goldie", age = 2},Cat {name = "Loopy", age = 4, lazy = False},Cat {name = "Sparky", age = 3, lazy = True}]}
Perhaps Peter is going on vacation and needs to leave his pets at home:
>>>
peter & set #pets []
Human {name = "Peter", age = 13, pets = []}
Technical details
LabelOptic
type class
class LabelOptic (name :: Symbol) k s t a b | name s -> k a, name t -> k b, name s b -> t, name t a -> s where Source #
Support for overloaded labels as optics. An overloaded label #foo
can be
used as an optic if there is an instance of
.LabelOptic
"foo" k s t a b
labelOptic :: Optic k NoIx s t a b Source #
Used to interpret overloaded label syntax. An overloaded label #foo
corresponds to
.labelOptic
@"foo"
Instances
(LabelOptic name k s t a b, GeneralLabelOptic name k s t a b (AnyHasRep (Rep s) (Rep t))) => LabelOptic name k s t a b Source # | If no instance matches, fall back on |
Defined in Optics.Internal.Optic labelOptic :: Optic k NoIx s t a b Source # |
type LabelOptic' name k s a = LabelOptic name k s s a a Source #
Type synonym for a type-preserving optic as overloaded label.
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
LabelOptic
uses the following functional dependencies to guarantee good
type inference:
name s -> k a
(the optic for the fieldname
ins
is of typek
and focuses ona
)name t -> k b
(the optic for the fieldname
int
is of typek
and focuses onb
)name s b -> t
(replacing the fieldname
ins
withb
yieldst
)name t a -> s
(replacing the fieldname
int
witha
yieldss
)
Dependencies (1) and (2) ensure 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.
Dependencies (3) and (4) ensure 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
ort
. - Modify type parameters of type
s
ort
ifa
orb
contain ambiguous applications of type families to these type parameters.