on cc5e99a help sign in

Generically derive traversals, lenses, and prisms.

git clone ssh://git.iko.soy/generic-lens

generic-lens

Generically derive lenses for accessing fields of data types.

Available on Hackage

This package uses the GHC 8 Generic representation to derive various operations on data structures with a lens interface, including structural subtype relationship between records and positional indexing into arbitrary product types.

This is made possible by GHC-8's new Generics API, which provides the meta-data at the type-level (previously only value-level meta-data was available).

An example can be found in the examples folder.

Record fields

Record fields can be accessed by their label.

data Person = Person { name :: String, age :: Int } deriving (Show, Generic)

sally :: Person
sally = Person "Sally" 25
>>> getField @"age" sally
25

>>> setField @"age" 26 sally
Person { name = "Sally", age = 26 }

>>> sally ^. label @"name"
"Sally"

>>> sally & label @"name" .~ "Tamas"
Person { name = "Tamas", age = 25 }

Positional fields

Fields can be accessed by their position in the data structure (index starting at 1).

data Point = Point Int Int Int deriving (Show, Generic)
data Polygon = Polygon Point Point Point deriving (Show, Generic)

polygon :: Polygon
polygon = Polygon (Point 1 5 3) (Point 2 4 2) (Point 5 7 (-2))

>>> getFieldAt @2 polygon
Point 2 4 2

>>> setFieldAt @1 (Point 26 5 3) polygon
Polygon (Point 26 5 3) (Point 2 4 2) (Point 5 7 (-2))

>>> polygon ^. itemAt @1 . itemAt @2
5

>>> polygon & itemAt @3 . itemAt @2 %~ (+10)
Polygon (Point 1 5 3) (Point 2 4 2) (Point 5 17 (-2))

Since tuples are an instance of Generic, positional lenses can be used with them.

>>> (("hello", True), 5) ^. itemAt @1 . itemAt @2
True

Structural subtyping

A record is a (structural) `subtype' of another, if its fields are a subset of those of the other.

data Human = Human
  { name    :: String
  , age     :: Int
  , address :: String
  } deriving (Generic, Show)

data Animal = Animal
  { name    :: String
  , age     :: Int
  } deriving (Generic, Show)

human :: Human
human = Human { name = "Tunyasz", age = 50, address = "London" }

>>> upcast human :: Animal
Animal {name = "Tunyasz", age = 50}

-- 'smash' plug the smaller structure into the larger one
>>> smash (Animal "dog" 10) human
Human {name = "dog", age = 10, address = "London"}

-- 'subtype' is a lens that focuses on a subrecord of a larger record:
>>> human ^. subtype @Animal
Animal {name = "Tunyasz", age = 50}

We can apply a function that operates on a subtype to the larger structure, by focusing on the subtype first:

growUp :: Animal -> Animal
growUp (Animal name age) = Animal name (age + 50)

>>> human & subtype @Animal %~ growUp
Human {name = "Tunyasz", age = 60, address = "London"}