optics-core-0.4.1.1: Optics as an abstract interface: core definitions

Safe Haskell | None
Language | Haskell2010

Optics.Label

Contents

- How to use labels as optics to make working with Haskell's records more convenient
  - The problem
  - The solution
  - The result
- Sample usage
- Technical details
  - `LabelOptic` type class
  - Structure of `LabelOptic` instances
  - Explanation of functional dependencies
- Orphan instances

Description

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 :: Symbol) k s a = LabelOptic name k s s a a
- class Generic a => GenericLabelOptics a where
  - type HasGenericLabelOptics a :: Bool

# 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:

1. Inability to define multiple data types sharing field names in the same module.
2. Pollution of global namespace as every field accessor is also a top-level function.
3. 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` and `OverloadedRecordDot` - introduced in GHC 9.2.1, addresses (2)
- `OverloadedRecordUpdate` - restricted version introduced in GHC 9.2.1, addresses (3)

It needs to be noted however that `OverloadedRecordUpdate` is not yet usable out of the box as it requires the user to enable `RebindableSyntax` and provide their own `HasField` class.

Is there no hope then for people who would like to work with records in a reasonable way without waiting? 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 `ghc-tags`, 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, seamlessly derived via generic representation (see the Sample usage section for more details) or generated with Template Haskell functions (defined in Optics.TH module from optics-th package) with

    makeFieldLabelsNoPrefix ''User
    makeFieldLabelsNoPrefix ''Movie

Generally speaking, both techniques trade blows in terms of compile time and run time resources. Generic optics are a bit slower to compile without optimizations than Template Haskell generated ones and their updating part might be slightly slower for larger data types with GHC < 9.2. On the other hand, generic optics are much more developer friendly.

Note: there exists a similar approach that involves prefixing field names (either with the underscore or name of the data type) and generation of lenses as ordinary functions so that `prefixField` 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. If you can't use GHC >= 9.2 and take advantage of the `NoFieldSelectors` language extension, this can be alleviated by splitting modules defining types into two, namely:

1. A private one that exports full type definitions, i.e. with their fields and constructors.
2. 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
                     }

    makeFieldLabelsNoPrefix ''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:

1. Prefixless field names that don't pollute global namespace (with the internal module qualification trick).
2. Working tags based jump-to-definition for field names (as `field` is the ordinary field, whereas `#field` is the lens referencing it).
3. 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 for fields of types having a `Generic` instance.

    >>> :set -XDeriveAnyClass
    >>> :set -XDeriveGeneric
    >>> :set -XDuplicateRecordFields
    >>> :set -XOverloadedLabels
    >>> import GHC.Generics (Generic)

    >>> :{
    data Pet
      = Cat  { name :: String, age :: Int, lazy :: Bool }
      | Fish { name :: String, age :: Int, lazy :: Bool }
      deriving (Show, Generic)
    :}

    >>> :{
    data Human = Human
      { name :: String
      , age  :: Integer
      , pets :: [Pet]
      } deriving (Show, Generic)
    :}

Note: Generic deriving of optics works well on a moderate scale, but for ubiquitous usage (and in production in general) we recommend generating them with Template Haskell as it scales better in terms of compilation time. For more details see `makeFieldLabelsNoPrefix` from Optics.TH in the optics-th package.

Here is some test data:

    >>> :{
    peter :: Human
    peter = Human { name = "Peter"
                  , age  = 13
                  , pets = [ Fish { name = "Goldie"
                                  , age  = 1
                                  , lazy = False
                                  }
                           , Cat { name = "Loopy"
                                 , age  = 3
                                 , lazy = False
                                 }
                           , Cat { name = "Sparky"
                                 , age  = 2
                                 , lazy = True
                                 }
                           ]
                 }
    :}

Now we can ask for Peter's name:

    >>> peter ^. #name
    "Peter"

or for names of his pets:

    >>> peter ^.. #pets % folded % #name
    ["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 & #age %~ (+1) & #pets % mapped % #age %~ (+1)
    Human {name = "Peter", age = 14, pets = [Fish {name = "Goldie", age = 2, lazy = False},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 & #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 ``LabelOptic` "foo" k s t a b`.

Alternatively, if both `s` and `t` have a `Generic` (`GenericLabelOptics` if `explicit-generic-labels` flag is enabled) instance, a total field of `s` is accessible by a label `#field` of kind `A_Lens`, whereas its constructor by a label `#_Constructor` of kind `A_Prism`.

Methods

labelOptic :: Optic k NoIx s t a b Source #

Used to interpret overloaded label syntax. An overloaded label `#foo` corresponds to ``labelOptic` @"foo"`.

#### Instances

Instances details

GenericLabelOpticContext repDefined name k s t a b => LabelOptic name k s t a b Source # | If no instance matches, try to use `Generic` machinery for field access. For more information have a look at `gfield` and `gconstructor`. Since: 0.4
Instance details Defined in Optics.Label Methods labelOptic :: Optic k NoIx s t a b Source #

type LabelOptic' (name :: Symbol) k s a = LabelOptic name k s s a a Source #

Type synonym for a type-preserving optic as overloaded label.

class Generic a => GenericLabelOptics a Source #

If the `explicit-generic-labels` Cabal flag is enabled, only types with this instance (which can be trivially derived with `DeriveAnyClass` extension) will be able to use labels as generic optics with a specific type.

It's an option for application developers to disable implicit fallback to generic optics for more control.

Libraries using generic labels with their data types should derive this instance for compatibility with the `explicit-generic-labels` flag.

Note: the flag `explicit-generic-labels` is disabled by default. Enabling it is generally unsupported as it might lead to compilation errors of dependencies relying on implicit fallback to generic optics.

Since: 0.4

Associated Types

type HasGenericLabelOptics a :: Bool Source #

type HasGenericLabelOptics a = 'True

## Structure of `LabelOptic` instances

You might wonder why instances generated with Template Haskell have the following 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 equality constraints are solved after the instance matches), which not only makes type inference better, but also allows it to generate better error messages.

    >>> :set -XDataKinds
    >>> :set -XFlexibleInstances
    >>> :set -XMultiParamTypeClasses
    >>> :set -XTypeFamilies
    >>> :set -XUndecidableInstances
    >>> :{
    data Pet = Dog { name :: String }
             | Cat { name :: String }
      deriving Show
    :}

    >>> :{
    data Human1 = Human1 { pets :: [Pet] }
      deriving Show
    instance LabelOptic "pets" A_Lens Human1 Human1 [Pet] [Pet] where
      labelOptic = lensVL $ \f (Human1 pets) -> Human1 <$> f pets
    :}

    >>> :{
    data Human2 = Human2 { pets :: [Pet] }
     deriving Show
    instance (k ~ A_Lens, a ~ [Pet], b ~ [Pet]) => LabelOptic "pets" k Human2 Human2 a b where
      labelOptic = lensVL $ \f (Human2 pets) -> Human2 <$> f pets
    :}

    >>> let human1 = Human1 [Dog "Lucky"]
    >>> let human2 = Human2 [Cat "Sleepy"]

Let's have a look how these two instance definitions differ.

    >>> human1 & #pets .~ []
    ...
    ...No instance for LabelOptic "pets" ‘A_Lens’ ‘Human1’ ‘()’ ‘[Pet]’ ‘[a0]’
    ...

    >>> human2 & #pets .~ []
    Human2 {pets = []}

That's because an empty list doesn't have a type `[Pet]`, it has a type `[r]` and GHC doesn't have enough information to match on the instance we provided. We'd need to either annotate the list:

    >>> human1 & #pets .~ ([] :: [Pet])
    Human1 {pets = []}

or the result type:

    >>> human1 & #pets .~ [] :: Human1
    Human1 {pets = []}

both of which are a nuisance.

Here are more examples of confusing error messages if the instance for `LabelOptic "pets"` is written without type equalities:

    >>> human1 ^. #pets :: Char
    ...
    ...No instance for LabelOptic "pets" ‘A_Lens’ ‘Human1’ ‘Human1’ ‘Char’ ‘Char’
    ...

    >>> human1 & #pets .~ 'x'
    ...
    ...No instance for LabelOptic "pets" ‘A_Lens’ ‘Human1’ ‘Human1’ ‘[Pet]’ ‘Char’
    ...

    >>> let pets = #pets :: Iso' Human1 [Pet]
    ...
    ...No instance for LabelOptic "pets" ‘An_Iso’ ‘Human1’ ‘Human1’ ‘[Pet]’ ‘[Pet]’
    ...

If we use the second form, error messages become much more accurate:

    >>> human2 ^. #pets :: Char
    ...
    ...Couldn't match type ‘Char’ with ‘[Pet]’
    ...  arising from the overloaded label ‘#pets’
    ...

    >>> human2 & #pets .~ 'x'
    ...
    ...Couldn't match type ‘Char’ with ‘[Pet]’
    ...  arising from the overloaded label ‘#pets’
    ...

    >>> let pets = #pets :: Iso' Human2 [Pet]
    ...
    ...Couldn't match type ‘An_Iso’ with ‘A_Lens’
    ...  arising from the overloaded label ‘#pets’
    ...

## Explanation of functional dependencies

`LabelOptic` uses the following functional dependencies to guarantee good type inference:

1. `name s -> k a` (the optic for the field `name` in `s` is of type `k` and focuses on `a`)
2. `name t -> k b` (the optic for the field `name` in `t` is of type `k` and focuses on `b`)
3. `name s b -> t` (replacing the field `name` in `s` with `b` yields `t`)
4. `name t a -> s` (replacing the field `name` in `t` with `a` yields `s`)

Dependencies (1) and (2) ensure that when we compose two optics, the middle type is unambiguous.

Dependencies (3) and (4) ensure that when we perform a chain of updates, the middle type is unambiguous.

# Orphan instances

(LabelOptic name k s t a b, is ~ NoIx) => IsLabel name (Optic k is s t a b) Source # |
Instance details Methods fromLabel :: Optic k is s t a b Source #