Safe Haskell | None |
---|---|
Language | Haskell2010 |
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)
- data Strictness
- data Field s l t = MkField {
- unField :: t
- type family (l :: Symbol) := (t :: *) = (f :: *) | f -> l t where ...
- type family (l :: Symbol) :=! (t :: *) = (f :: *) | f -> l t where ...
- pattern (:=) :: forall l t. KnownSymbol l => Proxy Symbol l -> t -> Field Lazy (Just Symbol l) t
- pattern (:=!) :: forall l t. KnownSymbol l => Proxy Symbol l -> t -> Field Strict (Just Symbol l) t
- pattern Field :: forall t. t -> Field Lazy (Nothing Symbol) t
- pattern Field' :: forall t. t -> Field Strict (Nothing Symbol) t
- type family R (t :: *) = (r :: *) where ...
- pattern R :: forall r t. ((:~) r (RImpl t), (:~) t (UnRImpl r)) => t -> r
- pattern P :: forall t r. (:~) t (UnRImpl r) => t -> r
- class (s :!! l) a | s l -> a where
- type (:*:) x y = x `RecMerge` y
- pattern (:*:) :: forall xs ys r. (:~) r ((::*:) xs ys) => xs -> ys -> r
- class (r :~ RecMergeImpl xs ys, (xs, ys) :~ RecPartitionImpl r (RecFieldList xs)) => (xs ::*: ys) r
- type (:~) r f = f r
Fields
A Field
consists of its strictness, an optional label (type-level Symbol
) and the field's type:
>>>
:kind Field
Field :: Strictness -> Maybe Symbol -> * -> *
Eq t => Eq (Field s l t) Source # | |
Ord t => Ord (Field s l t) Source # | |
Show t => Show (Field s (Nothing Symbol) t) Source # | |
(KnownSymbol l, Show t) => Show (Field Lazy (Just Symbol l) t) Source # | |
(KnownSymbol l, Show t) => Show (Field Strict (Just Symbol l) t) Source # | |
Generic (Field s l t) Source # | |
Monoid t => Monoid (Field s l t) Source # | |
NFData t => NFData (Field s l t) Source # | |
type Rep (Field s l t) Source # | |
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
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
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 '
, and performs a merge-sort using Lazy
'Nothing
t:*:
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 () ...
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] |
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!
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 :}
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)