@hackage labels0.1.0

Declare and access tuple fields with labels

labels

Declare and access tuple fields with labels

This package is experimental, exploring the design space opened up by the implemented and to-be-implemented work on extensible records in GHC.

Note: You need GHC 8.0.1 for the #foo syntax, otherwise you have to use $("foo") which works on GHC 7.10.

Basic examples

The haddock docs are here.

Enable these extensions:

  • In GHCi: :set -XOverloadedLabels -XTypeOperators -XDataKinds -XFlexibleContexts

  • In a module: {-# LANGUAGE OverloadedLabels, TypeOperators, DataKinds, FlexibleContexts #-}

Let's use GHCi:

> import Labels
> :set -XOverloadedLabels -XTypeOperators -XDataKinds -XFlexibleContexts
Construct a record
> (#foo := "hi", #bar := 123)
(#foo := "hi",#bar := 123)
Get fields of a record
> get #bar (#foo := "hi", #bar := 123)
123
Set fields of a record
> set #bar 66 (#foo := "hi", #bar := 123)
(#foo := "hi",#bar := 66)
Modify fields of a record
> modify #mu (*0.1) (#bar := "hi", #mu := 123)
(#bar := "hi",#mu := 12.3)
Add fields to a record
> cons (#mu := [1,2,3]) (#foo := "hi", #bar := 123)
(#mu := [1,2,3],#foo := "hi",#bar := 123)
Abstraction
> let double field record = set field (get field record * 2) record
> double #mu (#bar := "hi", #mu := 123)
(#bar := "hi",#mu := 246)

Reading CSV files with Cassava

Import the instances for FromNamedRecord:

import Labels.Cassava

Then just specify the type you want to load:

> let Right (_,rows :: Vector ("salary" := Int, "name" := Text)) = decodeByName "name,salary\r\nJohn,27\r\n"
> rows
[(#salary := 27,#name := "John")]

Non-existent fields or invalid types result in a parse error:

> decodeByName "name,salary\r\nJohn,27\r\n" :: Either String (Header, Vector ("name" := Text, "age" := Int))
Left "parse error (Failed reading: conversion error: Missing field age) at \"\\r\\n\""
> decodeByName "name,salary\r\nJohn,27\r\n" :: Either String (Header, Vector ("name" := Text, "salary" := Char))
Left "parse error (Failed reading: conversion error: expected Char, got \"27\") at \"\\r\\n\""

Example with Yahoo!'s market data for AAPL:

> Right (headers,rows :: Vector ("date" := String, "high" := Double, "low" := Double)) <- fmap decodeByName (LB.readFile "AAPL.csv")
> headers
["date","open","high","low","close","volume","adj close"]

We can print the rows as-is:

> mapM_ print (V.take 2 rows)
(#date := "2016-08-10",#high := 108.900002,#low := 107.760002)
(#date := "2016-08-09",#high := 108.940002,#low := 108.010002)

Accessing fields is natural as anything:

> V.sum (V.map (get #low) rows)
2331.789993

We can just make up new fields on the fly:

> let diffed = V.map (\row -> cons (#diff := (get #high row - get #low row)) row) rows
> mapM_ print (V.take 2 diffed)
(#diff := 1.1400000000000006,#date := "2016-08-10",#high := 108.900002,#low := 107.760002)
(#diff := 0.9300000000000068,#date := "2016-08-09",#high := 108.940002,#low := 108.010002)

Sometimes a CSV file will have non-valid Haskell identifiers or spaces, e.g. adj close here:

> Right (headers,rows :: Vector ("date" := String, "adj close" := Double)) <- fmap decodeByName (LB.readFile "AAPL.csv")
> mapM_ print (V.take 2 rows)
(#date := "2016-08-10",#adj close := 108.0)
(#date := "2016-08-09",#adj close := 108.809998)

Just use the $("adj close") syntax:

> mapM_ print (V.take 2 (V.map (get $("adj close")) rows))
108.0
108.809998

It still checks the name and type:

> mapM_ print (V.take 2 (V.map (get $("adj closer")) rows))
<interactive>:133:31: error:
    • No instance for (Has
                         "adj closer" a0 ("date" := String, "adj close" := Double))
        arising from a use of ‘get’