rawr-0.0.0.0: Anonymous extensible record and variant types

Safe HaskellNone
LanguageHaskell2010

Data.Rawr

Contents

Description

This package provides anonymous extensiblerecords. The major features and goals of this library are:

  • The syntax should be very close to that of Haskell's record system.
  • The library should be simple to use and requires no extra boilerplate to setup.
  • There should be minimal runtime overhead and memory footprint.
  • The library does not require TemplateHaskell to use.
  • It should produce good error messages.
  • Minimal dependencies. Currently, it only depends on packages that comes with GHC.

Prerequisite

You need GHC >= 8.0 and the following extensions in order to use this library:

>>> :set -XFlexibleContexts -XDataKinds -XOverloadedLabels -XScopedTypeVariables -XTypeOperators
>>> import Data.Rawr

as well as a few imports and extensions that will be used throughout this tutorial (but are not required to use this library):

>>> :set -XBangPatterns -XTypeFamilies
>>> import Control.Lens ((^.), (.~), (%~), (&))
>>> :m + Data.Monoid Data.Type.Equality

Record Types

The type function R is used to construct a record type:

>>> type Foo = R ( "a" := Int, "b" := Bool )

The above is analogous to the Haskell data declaration:

>>> data FooHs = FooHs { a :: Int, b :: Bool }

Note that the declaration order of fields doesn't matter. The library automatically sorts the fields by its labels, so the following two are equivalent:

>>> Refl :: R ( "a" := Int, "b" := Bool ) :~: R ( "b" := Bool, "a" := Int )
Refl

Records

The pattern R is used to construct a value of type Foo:

>>> let foo = R ( #a := 42, #b := True ) :: Foo

This is the same as:

>>> let fooHs = FooHs { a = 42, b = True } :: FooHs

As is the case of record type declaration, the order of fields isn't significant either:

>>> let foo2 = R ( #b := True, #a := 42 ) :: Foo

Attempting to construct a record with duplicate labels is a type error:

>>> R ( #a := 1, #a := 1 )

... error:
... Duplicate labels "a"
...

Selector Labels

Labels can be used as selectors:

>>> #a foo
42
>>> #b foo
True

Selecting a non-existant field is a type error:

>>> #c foo

... error:
... Label "c" does not occur in R ( "a" := Int, "b" := Bool )
...

Lenses

You can also use labels as van Laarhoven lens:

>>> foo ^. #a
42

Due to the way van Laarhoven lenses are defined, this library does not need to depend on the lens library, and you can use any of the alternative lens libraries that work with van Laarhoven lenses (microlens, lens-family, ..., etc).

Using label lenses on a non-existant field is a type error:

>>> foo ^. #c

... error:
... Label "c" does not occur in R ( "a" := Int, "b" := Bool )
...

Pattern Matching

You can pattern match on records too! However, as overloaded labels aren't supported in patterns (as of GHC 8.0.1), you need to supply the type annonation manually:

>>> case foo of R ( _ := b :: "b" := Bool, _ := a :: "a" := Int ) -> (a, b)
(42,True)

(Notice that the order is also insignificant here.)

Pseudo row-polymorphism

You can match parts of a record using P:

>>> case foo of r@(P ( _ := a :: "a" := Int )) -> r & #a .~ (a * 2)
R ( a := 84, b := True )

The difference is that while R needs to match on all fields on a record, P doesn't.

With PartialTypeSignatures, you may omit the types of fields in the signature if they can be inferred from the usage:

>>> :set -XPartialTypeSignatures -Wno-partial-type-signatures
>>> case foo of r@(P ( _ := a :: "a" := _ )) -> r & #a .~ (a * 2)
R ( a := 84, b := True )

Nested Records

Records can be arbitrarily nested:

>>> :{
       type User = R ( "id" := Int, "name" := String )
       type Post = R ( "id" := Int, "content" := String, "user" := User )
    :}
>>> :{
       let post = R ( #id := 123
                    , #content := "lorem ipsum"
                    , #user := R ( #id := 456
                                 , #name := "testuser"
                                 )
                    )
    :}

Although the id field is duplicated in both User and Post, both selector labels and lenses are overloaded and will do the right thing(tm):

>>> #id post
123
>>> #id (#user post)
456
>>> post ^. #user . #id
456
>>> post & #user . #name %~ (<> "2")
R ( content := "lorem ipsum", id := 123, user := R ( id := 456, name := "testuser2" ) )

Examples of error messages:

>>> post ^. #user . #error

... error:
... Label "error" does not occur in R ( "id" := ..., "name" := [Char] )
...
>>> post & #user . #error .~ "impossible"

... error:
... Label "error" does not occur in R ( "id" := ..., "name" := [Char] )
...

Extensible Records

You can merge two records together with :*::

>>> R ( #foo := True ) :*: R ( #bar := False )
R ( bar := False, foo := True )

Merging two records with duplicate labels is an error:

>>> R ( #foo := True ) :*: R ( #foo := True )

... error:
    • Duplicate labels "foo"
...

The same operator can be used to partition a record type as well; we can use this to model row-polymorphism:

>>> let f (R ( _ := a :: "a" := Int ) :*: _) = a * 2
>>> f $ R ( #a := (1 :: Int), #b := True )
2
>>> f $ R ( #a := (2 :: Int), #b := True, #c := False )
4

Renaming a field:

>>> let f (R ( _ := x :: "a" := Int ) :*: r) = R ( #x := x ) :*: r
>>> f $ R ( #a := (1 :: Int), #b := True )
R ( b := True, x := 1 )
>>> f $ R ( #a := (2 :: Int), #b := True, #c := False )
R ( b := True, c := False, x := 2 )
>>> f $ R ( #a := (3 :: Int), #x := True )

... error:
... Duplicate labels "x"
...

Strict Fields

To declare a field as strict, use :=! instead of :=.

>>> type Bar = R ( "a" :=! Int, "b" := Bool, "c" :=! Char )
>>> data BarHs = BarHs { a :: !Int, b :: Bool, c :: !Char }
>>> let bar = R ( #a :=! 42, #b := True, #c :=! 'c' ) :: Bar

Constructing a record where a strict field is bound to bottom is bottom:

>>> R ( #a := undefined ) `seq` ()
()
>>> R ( #a :=! undefined ) `seq` ()
*** Exception: Prelude.undefined
...

The current implementation of strict fields leaks the strictness info into the record's type. This implies that two records with same labels and types but different strictness properties aren't the same. (This may actually be a good thing?)

>>> Refl :: R ( "a" := () ) :~: R ( "a" :=! () )

... error:
... Couldn't match type ‘'Lazy’ with ‘'Strict’
...

Newtype

You can put records in a newtype:

>>> newtype Baz = Baz ( R ( "l" := Int ) )
>>> let baz = Baz $ R ( #l := 1 )

Or construct cyclic records:

>>> newtype C = C ( R ( "c" := C ) ) deriving Show
>>> let c = C $ R ( #c := c )
>>> putStrLn $ take 100 $ show c
C R ( c := C R ( c := C R ( c := C R ( c := C R ( c := C R ( c := C R ( c := C R ( c := C R ( c := C

Unlabeled Fields

It is also possible to have records with unlabeled fields, in this case, all operations are based on each field's position.

>>> let r = R (True, 42 :: Int, "foo" :: String, 'c')
>>> r
R ( True, 42, "foo", 'c' )
>>> case r of R (a :: Bool, b :: Int, c :: String, d :: Char) -> (a, b, c, d)
(True,42,"foo",'c')
>>> case r of R (a :: Bool, b :: Int) :*: _ -> (a, b)
(True,42)

Synopsis

Fields

data Strictness Source #

Strictness annonations for a Field.

Constructors

Lazy 
Strict 

data Field s l t Source #

A Field consists of its strictness, an optional label (type-level Symbol) and the field's type:

>>> :kind Field
Field :: Strictness -> Maybe Symbol -> * -> *

Constructors

MkField 

Fields

Instances

Eq t => Eq (Field s l t) Source # 

Methods

(==) :: Field s l t -> Field s l t -> Bool #

(/=) :: Field s l t -> Field s l t -> Bool #

Ord t => Ord (Field s l t) Source # 

Methods

compare :: Field s l t -> Field s l t -> Ordering #

(<) :: Field s l t -> Field s l t -> Bool #

(<=) :: Field s l t -> Field s l t -> Bool #

(>) :: Field s l t -> Field s l t -> Bool #

(>=) :: Field s l t -> Field s l t -> Bool #

max :: Field s l t -> Field s l t -> Field s l t #

min :: Field s l t -> Field s l t -> Field s l t #

Show t => Show (Field s (Nothing Symbol) t) Source # 
(KnownSymbol l, Show t) => Show (Field Lazy (Just Symbol l) t) Source # 

Methods

showsPrec :: Int -> Field Lazy (Just Symbol l) t -> ShowS #

show :: Field Lazy (Just Symbol l) t -> String #

showList :: [Field Lazy (Just Symbol l) t] -> ShowS #

(KnownSymbol l, Show t) => Show (Field Strict (Just Symbol l) t) Source # 
Generic (Field s l t) Source # 

Associated Types

type Rep (Field s l t) :: * -> * #

Methods

from :: Field s l t -> Rep (Field s l t) x #

to :: Rep (Field s l t) x -> Field s l t #

Monoid t => Monoid (Field s l t) Source # 

Methods

mempty :: Field s l t #

mappend :: Field s l t -> Field s l t -> Field s l t #

mconcat :: [Field s l t] -> Field s l t #

NFData t => NFData (Field s l t) Source # 

Methods

rnf :: Field s l t -> () #

type Rep (Field s l t) Source # 
type Rep (Field s l t) = D1 (MetaData "Field" "Data.Rawr" "rawr-0.0.0.0-DthLbpePQtN1RS8A3DcJQm" False) (C1 (MetaCons "MkField" PrefixI True) (S1 (MetaSel (Just Symbol "unField") NoSourceUnpackedness NoSourceStrictness DecidedLazy) (Rec0 t)))

type family (l :: Symbol) := (t :: *) = (f :: *) | f -> l t where ... infix 2 Source #

A labeled lazy field.

>>> :kind! "foo" := Int
"foo" := Int :: *
= Field 'Lazy ('Just "foo") Int

Equations

l := t = Field Lazy (Just l) t 

type family (l :: Symbol) :=! (t :: *) = (f :: *) | f -> l t where ... infix 2 Source #

A labeled strict field.

>>> :kind! "foo" :=! Int
"foo" :=! Int :: *
= Field 'Strict ('Just "foo") Int

Equations

l :=! t = Field Strict (Just l) t 

Patterns for fields

pattern (:=) :: forall l t. KnownSymbol l => Proxy Symbol l -> t -> Field Lazy (Just Symbol l) t infix 2 Source #

Construct or pattern-match a lazy labeled field.

>>> :t #foo := True
#foo := True :: Field 'Lazy ('Just "foo") Bool

pattern (:=!) :: forall l t. KnownSymbol l => Proxy Symbol l -> t -> Field Strict (Just Symbol l) t infix 2 Source #

Construct or pattern-match a strict labeled field.

>>> :t #foo :=! True
#foo :=! True :: Field 'Strict ('Just "foo") Bool

pattern Field :: forall t. t -> Field Lazy (Nothing Symbol) t Source #

Construct or pattern-match a lazy unlabeled field.

>>> :t Field True
Field True :: Field 'Lazy 'Nothing Bool

pattern Field' :: forall t. t -> Field Strict (Nothing Symbol) t Source #

Strict version of Field.

>>> :t Field' True
Field' True :: Field 'Strict 'Nothing Bool

This can be used to construct a strict tuple:

>>> let !r  = R ( True, undefined :: Int )
>>> case r of R ( a :: Bool ) :*: _ -> a
True
>>> let !r' = R ( Field' True, Field' (undefined :: Int ) )
*** Exception: Prelude.undefined
...

Records

A record is internally represented as a data family indexed by a list of Field:

 data    family   Rec (xs :: [*])
 data    instance Rec '[] = R0
 newtype instance Rec '[Field s0 l0 t0] = R1 (Field s0 l0 t0)
 data    instance Rec '[Field s0 l0 t0, Field s1 l1 t1] = R2 {-# UNPACK #-} !(Field s0 l0 t0) {-# UNPACK #-} !(Field s1 l1 t1)
 ...

The UNPACK pragmas ensure that Field's constructor is erased at runtime, thus the following record:

 Rec '[ "a" := Int, "b" := Bool, "c" := String ]

has the same memory footprint as:

 (Int, Bool, String)

or:

 data Foo = Foo { a :: Int, b :: Bool, c :: String }

(See test-suite "datasize".)

A record can be either:

  • A labeled record: All of its fields are labeled and its order sorted using CmpSymbol.
  • An unlabeled record: In this case all fields are unlabeled and indexed by their positions, and if all fields are lazy, they are isomorphic to Haskell tuples.

Mixing labeled and unlabeled fields isn't allowed. This is enforced by the library's smart constructors.

Eq, Ord, Show, Monoid, NFData instances are provided if all of the fields are also instances of respective classes.

Records with up to 8 fields are supported currently.

type family R (t :: *) = (r :: *) where ... Source #

R takes a tuple, where each non-Field element a is wrapped as a lazy non-labeled field Field 'Lazy 'Nothing t, and performs a merge-sort using :*: if the fields are labeled.

>>> :kind! R ( "foo" := Bool , "bar" := Int )
R ( "foo" := Bool , "bar" := Int ) :: *
= Rec
    '[Field 'Lazy ('Just "bar") Int, Field 'Lazy ('Just "foo") Bool]
>>> :kind! R (Int, Bool)
R (Int, Bool) :: *
= Rec '[Field 'Lazy 'Nothing Int, Field 'Lazy 'Nothing Bool]

GHC should be capable of inlining most of the label-sorting away, therefore the following expression:

 R ( #e := (), #d := (), #c := (), #b := (), #a := () )

should have similar performance as:

 (\(e, d, c, b, a) -> (a, b, c, d, e)) ( #e := (), #d := (), #c := (), #b := (), #a := () )

Matching a field that does not occur in the record is an error:

>>> case R () of R ( _ :: "a" := Int ) -> ()

... error:
... Label "a" does not occur in R ()
...

Equations

R () = Rec '[] 
R (a, b) = Rec '[ToField a] :*: Rec '[ToField b] 
R (a, b, c) = Rec '[ToField a] :*: (Rec '[ToField b] :*: Rec '[ToField c]) 
R (a, b, c, d) = (Rec '[ToField a] :*: Rec '[ToField b]) :*: (Rec '[ToField c] :*: Rec '[ToField d]) 
R (a, b, c, d, e) = (Rec '[ToField a] :*: Rec '[ToField b]) :*: (Rec '[ToField c] :*: (Rec '[ToField d] :*: Rec '[ToField e])) 
R (a, b, c, d, e, f) = (Rec '[ToField a] :*: (Rec '[ToField b] :*: Rec '[ToField c])) :*: (Rec '[ToField d] :*: (Rec '[ToField e] :*: Rec '[ToField f])) 
R (a, b, c, d, e, f, g) = (Rec '[ToField a] :*: (Rec '[ToField b] :*: Rec '[ToField c])) :*: ((Rec '[ToField d] :*: Rec '[ToField e]) :*: (Rec '[ToField f] :*: Rec '[ToField g])) 
R (a, b, c, d, e, f, g, h) = ((Rec '[ToField a] :*: Rec '[ToField b]) :*: (Rec '[ToField c] :*: Rec '[ToField d])) :*: ((Rec '[ToField e] :*: Rec '[ToField f]) :*: (Rec '[ToField g] :*: Rec '[ToField h])) 
R a = Rec '[ToField a] 

pattern R :: forall r t. ((:~) r (RImpl t), (:~) t (UnRImpl r)) => t -> r Source #

When R is used as a constructor, it is equivalent to the type family R, except that it performs the operations at value-level.

As a pattern, R destructs all fields of a record into a tuple of Fields. In the case of labeled record, the Fields can be in arbitrarily order.

pattern P :: forall t r. (:~) t (UnRImpl r) => t -> r Source #

Given a tuple of Fields, P binds the matching fields from the record. (Note: Partial matching is only available for labeled records.)

Indexing records

class (s :!! l) a | s l -> a where Source #

(:!!) s l a says that the record s has a field of type a at index l, and provides a Lens' s a to get/set that particular field.

If you are thinking that the syntax is ugly, we can use the utility operator :~ to write a :~ (s :!! l) which is roughly equivalent to the equality constraint a ~ (s !! t). Nice!

Minimal complete definition

fieldLens

Methods

fieldLens :: Lens' s a Source #

Instances

(~) * a (TypeError * ((:<>:) ((:<>:) ((:<>:) (Text "Label ") (ShowType Symbol l)) (Text " does not occur in ")) (PPRec s))) => (s :!! l) a Source # 

Methods

fieldLens :: Lens' s a Source #

Merging & partitioning records

type (:*:) x y = x `RecMerge` y infixr 1 Source #

Merge two records types.

>>> :kind! R ( "foo" := Int ) :*: R ( "bar" := Bool )
R ( "foo" := Int ) :*: R ( "bar" := Bool ) :: *
= Rec
    '[Field 'Lazy ('Just "bar") Bool, Field 'Lazy ('Just "foo") Int]
>>> :kind! R ( Field 'Lazy 'Nothing Int ) :*: ( Field 'Strict 'Nothing Bool )
R ( Field 'Lazy 'Nothing Int ) :*: ( Field 'Strict 'Nothing Bool ) :: GHC.Types.*
= Rec '[Field 'Lazy 'Nothing Int] :*: Field 'Strict 'Nothing Bool

pattern (:*:) :: forall xs ys r. (:~) r ((::*:) xs ys) => xs -> ys -> r infixr 1 Source #

As a constructor, merge two records.

>>> R ( #foo := (1 :: Int) ) :*: R ( #bar := True )
R ( bar := True, foo := 1 )
>>> R ( 1 :: Int ) :*: R ( True )
R ( 1, True )

Merging labeled and unlabeled records is an error:

>>> R ( True ) :*: R ( #foo := True )

... error:
... RecMerge: Cannot merge labeled and unlabeled fields
...

As a pattern, partition a record based on the type of LHS.

>>> let r = R ( #a := (1 :: Int), #b := True, #c := "hello world" )
>>> case r of R ( _ := a :: "a" := Int, _ := c :: "c" := String ) :*: _ -> (a, c)
(1,"hello world")

This means that you can't write the above example as:

>>> case r of _ :*: R ( _ := a :: "a" := Int, _ := c :: "c" := String ) -> (a, c)

... error:
... Ambiguous type variable ‘...’ arising from a pattern
...

Mismatches between the LHS and the record result in type errors:

>>> case R () of R ( _ :: "a" := Int ) :*: _ -> ()

... error:
... RecPartition: Label "a" does not occur in the record
...
>>> case R () of R ( a :: Int ) :*: _ -> ()

... error:
... RecPartition: Not enough fields in the record
...
>>> case R ( #a := True, #b := (1 :: Int) ) of R ( _ :: "a" := Int ) :*: _ -> ()

... error:
... RecParition: type mismatch between Bool and Int for label "a"
...
>>> case R ( True, 1 :: Int ) of R ( a :: Int ) :*: _ -> ()

... error:
... RecPartition: type mismatch between Bool and Int
...

class (r :~ RecMergeImpl xs ys, (xs, ys) :~ RecPartitionImpl r (RecFieldList xs)) => (xs ::*: ys) r infix 1 Source #

A utility constraint for you to write signatures involving :*:. For example, the following function that deletes the field with label a has the signature:

>>> :{
       let f :: (r :~ R ( "a" := Int ) ::*: ys) => r -> ys
           f (R ( _ :: "a" := Int) :*: ys) = ys
    :}

Instances

((:~) r (RecMergeImpl xs ys), (:~) (xs, ys) (RecPartitionImpl r (RecFieldList xs))) => (xs ::*: ys) r Source # 

Utilities

type (:~) r f = f r infix 0 Source #

A helper to convert functional dependencies into nicer type-equality-like syntax.

 (:!!) s a t

is equivalent to

 t :~ (s :!! a)

which is roughly equivalent to:

 t ~ (s !! a)

Orphan instances

(~) Symbol l l' => IsLabel l (Proxy Symbol l') Source # 

Methods

fromLabel :: Proxy# Symbol l -> Proxy Symbol l' #

((:~) a ((:!!) s l), Functor f, (~) * a b, (~) * s2fs (s -> f s)) => IsLabel l ((a -> f b) -> s2fs) Source # 

Methods

fromLabel :: Proxy# Symbol l -> (a -> f b) -> s2fs #