colonnade-1.2.0: Generic types and functions for columnar encoding and decoding

Safe HaskellNone
LanguageHaskell2010

Colonnade

Contents

Description

Build backend-agnostic columnar encodings that can be used to visualize tabular data.

Synopsis

Example

First, let's bring in some neccessary imports that will be used for the remainder of the examples in the docs:

>>> import Data.Monoid (mconcat,(<>))
>>> import Data.Profunctor (lmap)

The data types we wish to encode are:

>>> data Color = Red | Green | Blue deriving (Show,Eq)
>>> data Person = Person { name :: String, age :: Int }
>>> data House = House { color :: Color, price :: Int }

One potential columnar encoding of a Person would be:

>>> :{
let colPerson :: Colonnade Headed Person String
    colPerson = mconcat
      [ headed "Name" name
      , headed "Age" (show . age)
      ]
:}

The type signature on colPerson is not neccessary but is included for clarity. We can feed data into this encoding to build a table:

>>> let people = [Person "David" 63, Person "Ava" 34, Person "Sonia" 12]
>>> putStr (ascii colPerson people)
+-------+-----+
| Name  | Age |
+-------+-----+
| David | 63  |
| Ava   | 34  |
| Sonia | 12  |
+-------+-----+

Similarly, we can build a table of houses with:

>>> let showDollar = (('$':) . show) :: Int -> String
>>> colHouse = mconcat [headed "Color" (show . color), headed "Price" (showDollar . price)]
>>> :t colHouse
colHouse :: Colonnade Headed House [Char]
>>> let houses = [House Green 170000, House Blue 115000, House Green 150000]
>>> putStr (ascii colHouse houses)
+-------+---------+
| Color | Price   |
+-------+---------+
| Green | $170000 |
| Blue  | $115000 |
| Green | $150000 |
+-------+---------+

data Colonnade h a c Source #

An columnar encoding of a. The type variable h determines what is present in each column in the header row. It is typically instantiated to Headed and occasionally to Headless. There is nothing that restricts it to these two types, although they satisfy the majority of use cases. The type variable c is the content type. This can be Text, String, or ByteString. In the companion libraries reflex-dom-colonnade and yesod-colonnade, additional types that represent HTML with element attributes are provided that serve as the content type. Presented more visually:

            +---- Value consumed to build a row
            |
            v
Colonnade h a c
          ^   ^
          |   |
          |   +-- Content (Text, ByteString, Html, etc.)
          |
          +------ Headedness (Headed or Headless)

Internally, a Colonnade is represented as a Vector of individual column encodings. It is possible to use any collection type with Alternative and Foldable instances. However, Vector was chosen to optimize the data structure for the use case of building the structure once and then folding over it many times. It is recommended that Colonnades are defined at the top-level so that GHC avoids reconstructing them every time they are used.

Instances

Functor h => Profunctor (Colonnade h) Source # 

Methods

dimap :: (a -> b) -> (c -> d) -> Colonnade h b c -> Colonnade h a d #

lmap :: (a -> b) -> Colonnade h b c -> Colonnade h a c #

rmap :: (b -> c) -> Colonnade h a b -> Colonnade h a c #

(#.) :: Coercible * c b => (b -> c) -> Colonnade h a b -> Colonnade h a c #

(.#) :: Coercible * b a => Colonnade h b c -> (a -> b) -> Colonnade h a c #

Functor h => Functor (Colonnade h a) Source # 

Methods

fmap :: (a -> b) -> Colonnade h a a -> Colonnade h a b #

(<$) :: a -> Colonnade h a b -> Colonnade h a a #

Semigroup (Colonnade h a c) Source # 

Methods

(<>) :: Colonnade h a c -> Colonnade h a c -> Colonnade h a c #

sconcat :: NonEmpty (Colonnade h a c) -> Colonnade h a c #

stimes :: Integral b => b -> Colonnade h a c -> Colonnade h a c #

Monoid (Colonnade h a c) Source # 

Methods

mempty :: Colonnade h a c #

mappend :: Colonnade h a c -> Colonnade h a c -> Colonnade h a c #

mconcat :: [Colonnade h a c] -> Colonnade h a c #

newtype Headed a Source #

As the first argument to the Colonnade type constructor, this indictates that the columnar encoding has a header. This type is isomorphic to Identity but is given a new name to clarify its intent:

example :: Colonnade Headed Foo Text

The term example represents a columnar encoding of Foo in which the columns have headings.

Constructors

Headed 

Fields

Instances

Functor Headed Source # 

Methods

fmap :: (a -> b) -> Headed a -> Headed b #

(<$) :: a -> Headed b -> Headed a #

Applicative Headed Source # 

Methods

pure :: a -> Headed a #

(<*>) :: Headed (a -> b) -> Headed a -> Headed b #

liftA2 :: (a -> b -> c) -> Headed a -> Headed b -> Headed c #

(*>) :: Headed a -> Headed b -> Headed b #

(<*) :: Headed a -> Headed b -> Headed a #

Foldable Headed Source # 

Methods

fold :: Monoid m => Headed m -> m #

foldMap :: Monoid m => (a -> m) -> Headed a -> m #

foldr :: (a -> b -> b) -> b -> Headed a -> b #

foldr' :: (a -> b -> b) -> b -> Headed a -> b #

foldl :: (b -> a -> b) -> b -> Headed a -> b #

foldl' :: (b -> a -> b) -> b -> Headed a -> b #

foldr1 :: (a -> a -> a) -> Headed a -> a #

foldl1 :: (a -> a -> a) -> Headed a -> a #

toList :: Headed a -> [a] #

null :: Headed a -> Bool #

length :: Headed a -> Int #

elem :: Eq a => a -> Headed a -> Bool #

maximum :: Ord a => Headed a -> a #

minimum :: Ord a => Headed a -> a #

sum :: Num a => Headed a -> a #

product :: Num a => Headed a -> a #

Headedness Headed Source # 
Eq a => Eq (Headed a) Source # 

Methods

(==) :: Headed a -> Headed a -> Bool #

(/=) :: Headed a -> Headed a -> Bool #

Ord a => Ord (Headed a) Source # 

Methods

compare :: Headed a -> Headed a -> Ordering #

(<) :: Headed a -> Headed a -> Bool #

(<=) :: Headed a -> Headed a -> Bool #

(>) :: Headed a -> Headed a -> Bool #

(>=) :: Headed a -> Headed a -> Bool #

max :: Headed a -> Headed a -> Headed a #

min :: Headed a -> Headed a -> Headed a #

Read a => Read (Headed a) Source # 
Show a => Show (Headed a) Source # 

Methods

showsPrec :: Int -> Headed a -> ShowS #

show :: Headed a -> String #

showList :: [Headed a] -> ShowS #

data Headless a Source #

As the first argument to the Colonnade type constructor, this indictates that the columnar encoding does not have a header. This type is isomorphic to Proxy but is given a new name to clarify its intent:

example :: Colonnade Headless Foo Text

The term example represents a columnar encoding of Foo in which the columns do not have headings.

Constructors

Headless 

Instances

Functor Headless Source # 

Methods

fmap :: (a -> b) -> Headless a -> Headless b #

(<$) :: a -> Headless b -> Headless a #

Applicative Headless Source # 

Methods

pure :: a -> Headless a #

(<*>) :: Headless (a -> b) -> Headless a -> Headless b #

liftA2 :: (a -> b -> c) -> Headless a -> Headless b -> Headless c #

(*>) :: Headless a -> Headless b -> Headless b #

(<*) :: Headless a -> Headless b -> Headless a #

Foldable Headless Source # 

Methods

fold :: Monoid m => Headless m -> m #

foldMap :: Monoid m => (a -> m) -> Headless a -> m #

foldr :: (a -> b -> b) -> b -> Headless a -> b #

foldr' :: (a -> b -> b) -> b -> Headless a -> b #

foldl :: (b -> a -> b) -> b -> Headless a -> b #

foldl' :: (b -> a -> b) -> b -> Headless a -> b #

foldr1 :: (a -> a -> a) -> Headless a -> a #

foldl1 :: (a -> a -> a) -> Headless a -> a #

toList :: Headless a -> [a] #

null :: Headless a -> Bool #

length :: Headless a -> Int #

elem :: Eq a => a -> Headless a -> Bool #

maximum :: Ord a => Headless a -> a #

minimum :: Ord a => Headless a -> a #

sum :: Num a => Headless a -> a #

product :: Num a => Headless a -> a #

Contravariant Headless Source # 

Methods

contramap :: (a -> b) -> Headless b -> Headless a #

(>$) :: b -> Headless b -> Headless a #

Headedness Headless Source # 
Eq (Headless a) Source # 

Methods

(==) :: Headless a -> Headless a -> Bool #

(/=) :: Headless a -> Headless a -> Bool #

Ord (Headless a) Source # 

Methods

compare :: Headless a -> Headless a -> Ordering #

(<) :: Headless a -> Headless a -> Bool #

(<=) :: Headless a -> Headless a -> Bool #

(>) :: Headless a -> Headless a -> Bool #

(>=) :: Headless a -> Headless a -> Bool #

max :: Headless a -> Headless a -> Headless a #

min :: Headless a -> Headless a -> Headless a #

Read (Headless a) Source # 
Show (Headless a) Source # 

Methods

showsPrec :: Int -> Headless a -> ShowS #

show :: Headless a -> String #

showList :: [Headless a] -> ShowS #

Typeclasses

class Headedness h where Source #

This class communicates that a container holds either zero elements or one element. Furthermore, all inhabitants of the type must hold the same number of elements. Both Headed and Headless have instances. The following law accompanies any instances:

maybe x (\f -> f (headednessPure x)) headednessContents == x
todo: come up with another law that relates to Traversable

Consequently, there is no instance for Maybe, which cannot satisfy the laws since it has inhabitants which hold different numbers of elements. Nothing holds 0 elements and Just holds 1 element.

Create

headed :: c -> (a -> c) -> Colonnade Headed a c Source #

A single column with a header.

headless :: (a -> c) -> Colonnade Headless a c Source #

A single column without a header.

singleton :: h c -> (a -> c) -> Colonnade h a c Source #

A single column with any kind of header. This is not typically needed.

Transform

Body

fromMaybe :: c -> Colonnade f a c -> Colonnade f (Maybe a) c Source #

Lift a column over a Maybe. For example, if some people have houses and some do not, the data that pairs them together could be represented as:

>>> :{
let owners :: [(Person,Maybe House)]
    owners =
      [ (Person "Jordan" 18, Nothing)
      , (Person "Ruth" 25, Just (House Red 125000))
      , (Person "Sonia" 12, Just (House Green 145000))
      ]
:}

The column encodings defined earlier can be reused with the help of fromMaybe:

>>> :{
let colOwners :: Colonnade Headed (Person,Maybe House) String
    colOwners = mconcat
      [ lmap fst colPerson
      , lmap snd (fromMaybe "" colHouse)
      ]
:}
>>> putStr (ascii colOwners owners)
+--------+-----+-------+---------+
| Name   | Age | Color | Price   |
+--------+-----+-------+---------+
| Jordan | 18  |       |         |
| Ruth   | 25  | Red   | $125000 |
| Sonia  | 12  | Green | $145000 |
+--------+-----+-------+---------+

columns Source #

Arguments

:: Foldable g 
=> (b -> a -> c)

Cell content function

-> (b -> f c)

Header content function

-> g b

Basis for column encodings

-> Colonnade f a c 

Convert a collection of b values into a columnar encoding of the same size. Suppose we decide to show a house's color by putting a check mark in the column corresponding to the color instead of by writing out the name of the color:

>>> let allColors = [Red,Green,Blue]
>>> let encColor = columns (\c1 c2 -> if c1 == c2 then "✓" else "") (Headed . show) allColors
>>> :t encColor
encColor :: Colonnade Headed Color [Char]
>>> let encHouse = headed "Price" (showDollar . price) <> lmap color encColor
>>> :t encHouse
encHouse :: Colonnade Headed House [Char]
>>> putStr (ascii encHouse houses)
+---------+-----+-------+------+
| Price   | Red | Green | Blue |
+---------+-----+-------+------+
| $170000 |     | ✓     |      |
| $115000 |     |       | ✓    |
| $150000 |     | ✓     |      |
+---------+-----+-------+------+

bool Source #

Arguments

:: f c

Heading

-> (a -> Bool)

Predicate

-> (a -> c)

Contents when predicate is false

-> (a -> c)

Contents when predicate is true

-> Colonnade f a c 

replaceWhen Source #

Arguments

:: c

New content

-> (a -> Bool)

Row predicate

-> Colonnade f a c

Original Colonnade

-> Colonnade f a c 

Replace the contents of cells in rows whose values satisfy the given predicate. Header content is unaffected.

modifyWhen Source #

Arguments

:: (c -> c)

Content change

-> (a -> Bool)

Row predicate

-> Colonnade f a c

Original Colonnade

-> Colonnade f a c 

Modify the contents of cells in rows whose values satisfy the given predicate. Header content is unaffected. With an HTML backend, this can be used to strikethrough the contents of cells with data that is considered invalid.

Header

mapHeaderContent :: Functor h => (c -> c) -> Colonnade h a c -> Colonnade h a c Source #

Map over the content in the header. This is similar performing fmap on a Colonnade except that the body content is unaffected.

mapHeadedness :: (forall x. h x -> h' x) -> Colonnade h a c -> Colonnade h' a c Source #

Map over the header type of a Colonnade.

toHeadless :: Colonnade h a c -> Colonnade Headless a c Source #

Remove the heading from a Colonnade.

Cornice

Types

data Cornice h (p :: Pillar) a c Source #

Instances

Functor h => Profunctor (Cornice h p) Source # 

Methods

dimap :: (a -> b) -> (c -> d) -> Cornice h p b c -> Cornice h p a d #

lmap :: (a -> b) -> Cornice h p b c -> Cornice h p a c #

rmap :: (b -> c) -> Cornice h p a b -> Cornice h p a c #

(#.) :: Coercible * c b => (b -> c) -> Cornice h p a b -> Cornice h p a c #

(.#) :: Coercible * b a => Cornice h p b c -> (a -> b) -> Cornice h p a c #

Functor h => Functor (Cornice h p a) Source # 

Methods

fmap :: (a -> b) -> Cornice h p a a -> Cornice h p a b #

(<$) :: a -> Cornice h p a b -> Cornice h p a a #

Semigroup (Cornice h p a c) Source # 

Methods

(<>) :: Cornice h p a c -> Cornice h p a c -> Cornice h p a c #

sconcat :: NonEmpty (Cornice h p a c) -> Cornice h p a c #

stimes :: Integral b => b -> Cornice h p a c -> Cornice h p a c #

ToEmptyCornice p => Monoid (Cornice h p a c) Source # 

Methods

mempty :: Cornice h p a c #

mappend :: Cornice h p a c -> Cornice h p a c -> Cornice h p a c #

mconcat :: [Cornice h p a c] -> Cornice h p a c #

data Pillar Source #

Isomorphic to the natural numbers. Only the promoted version of this type is used.

Constructors

Cap !Pillar 
Base 

data Fascia (p :: Pillar) r where Source #

Constructors

FasciaBase :: !r -> Fascia Base r 
FasciaCap :: !r -> Fascia p r -> Fascia (Cap p) r 

Create

cap :: c -> Colonnade h a c -> Cornice h (Cap Base) a c Source #

Augment a Colonnade with a header spans over all of the existing headers. This is best demonstrated by example. Let's consider how we might encode a pairing of the people and houses from the initial example:

>>> let personHomePairs = zip people houses
>>> let colPersonFst = lmap fst colPerson
>>> let colHouseSnd = lmap snd colHouse
>>> putStr (ascii (colPersonFst <> colHouseSnd) personHomePairs)
+-------+-----+-------+---------+
| Name  | Age | Color | Price   |
+-------+-----+-------+---------+
| David | 63  | Green | $170000 |
| Ava   | 34  | Blue  | $115000 |
| Sonia | 12  | Green | $150000 |
+-------+-----+-------+---------+

This tabular encoding leaves something to be desired. The heading not indicate that the name and age refer to a person and that the color and price refer to a house. Without reaching for Cornice, we can still improve this situation with mapHeaderContent:

>>> let colPersonFst' = mapHeaderContent ("Person " ++) colPersonFst
>>> let colHouseSnd' = mapHeaderContent ("House " ++) colHouseSnd
>>> putStr (ascii (colPersonFst' <> colHouseSnd') personHomePairs)
+-------------+------------+-------------+-------------+
| Person Name | Person Age | House Color | House Price |
+-------------+------------+-------------+-------------+
| David       | 63         | Green       | $170000     |
| Ava         | 34         | Blue        | $115000     |
| Sonia       | 12         | Green       | $150000     |
+-------------+------------+-------------+-------------+

This is much better, but for longer tables, the redundancy of prefixing many column headers can become annoying. The solution that a Cornice offers is to nest headers:

>>> let cor = mconcat [cap "Person" colPersonFst, cap "House" colHouseSnd]
>>> :t cor
cor :: Cornice Headed ('Cap 'Base) (Person, House) [Char]
>>> putStr (asciiCapped cor personHomePairs)
+-------------+-----------------+
| Person      | House           |
+-------+-----+-------+---------+
| Name  | Age | Color | Price   |
+-------+-----+-------+---------+
| David | 63  | Green | $170000 |
| Ava   | 34  | Blue  | $115000 |
| Sonia | 12  | Green | $150000 |
+-------+-----+-------+---------+

recap :: c -> Cornice h p a c -> Cornice h (Cap p) a c Source #

Add another cap to a cornice. There is no limit to how many times this can be applied:

>>> data Day = Weekday | Weekend deriving (Show)
>>> :{
let cost :: Int -> Day -> String
    cost base w = case w of
      Weekday -> showDollar base
      Weekend -> showDollar (base + 1)
    colStandard = foldMap (\c -> headed c (cost 8)) ["Yt","Ad","Sr"]
    colSpecial = mconcat [headed "Stud" (cost 6), headed "Mltry" (cost 7)]
    corStatus = mconcat
      [ cap "Standard" colStandard
      , cap "Special" colSpecial
      ] 
    corShowtime = mconcat
      [ recap "" (cap "" (headed "Day" show))
      , foldMap (\c -> recap c corStatus) ["Matinee","Evening"]
      ]
:}
>>> putStr (asciiCapped corShowtime [Weekday,Weekend])
+---------+-----------------------------+-----------------------------+
|         | Matinee                     | Evening                     |
+---------+--------------+--------------+--------------+--------------+
|         | Standard     | Special      | Standard     | Special      |
+---------+----+----+----+------+-------+----+----+----+------+-------+
| Day     | Yt | Ad | Sr | Stud | Mltry | Yt | Ad | Sr | Stud | Mltry |
+---------+----+----+----+------+-------+----+----+----+------+-------+
| Weekday | $8 | $8 | $8 | $6   | $7    | $8 | $8 | $8 | $6   | $7    |
| Weekend | $9 | $9 | $9 | $7   | $8    | $9 | $9 | $9 | $7   | $8    |
+---------+----+----+----+------+-------+----+----+----+------+-------+

Ascii Table

ascii Source #

Arguments

:: Foldable f 
=> Colonnade Headed a String

columnar encoding

-> f a

rows

-> String 

Render a collection of rows as an ascii table. The table's columns are specified by the given Colonnade. This implementation is inefficient and does not provide any wrapping behavior. It is provided so that users can try out colonnade in ghci and so that doctest can verify example code in the haddocks.

asciiCapped Source #

Arguments

:: Foldable f 
=> Cornice Headed p a String

columnar encoding

-> f a

rows

-> String