Skip to content

mistivia/flex-record

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

17 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

flex-record

flex-record is a lightweight Haskell Record library based on type-level field lists. It provides:

  • Type-safe field access (frGet)
  • Type-safe field update (frSet)
  • Integration with accessor-hs (frAcc)

Quick Start

FlexRecord

import FlexRecord (Field, FlexRecord)

type Person = FlexRecord
    [ Field "name"   String
    , Field "age"    Int
    , Field "height" Float
    , Field "scores" [Int]
    ]

Use flexRecord + field to build from an empty record via function composition:

import FlexRecord (field, flexRecord)

person :: Person
person = flexRecord
    $ field @"name" "ABC"
    . field @"age" 1
    . field @"height" 42.0
    . field @"scores" [ 10, 20, 30]

Read and update fields:

import FlexRecord (frGet, frSet)

age1 :: Int
age1 = frGet @"age" person
-- 1

person2 :: Person
person2 = frSet @"age" 2 person

age2 :: Int
age2 = frGet @"age" person2
-- 2

frGet / frSet are both type-safe:
if a field name does not exist, compilation fails.

Using with accessor-hs:

frAcc turns a field into an Accessor, so you can continue using view / set / over / dot / facc.

import qualified Data.Accessor as Accessor
import Data.Accessor (facc, over, set, view)
import Data.FlexRecord (frAcc)

ageAcc = frAcc @"age"

firstScoreAcc = frAcc @"scores" . Accessor._0

eachScoreAcc = frAcc @"scores" . facc

Usage examples:

view ageAcc person
-- 1

frGet @"age" (set ageAcc 3 person)
-- 3

frGet @"age" (over ageAcc (+ 10) person)
-- 11

view firstScoreAcc person
-- 10

frGet @"scores" (set firstScoreAcc 99 person)
-- [99,20,30]

frGet @"scores" (over eachScoreAcc (+ 1) person)
-- [11,21,31]

FlexEnum

FlexEnum is a variant type (tagged union) that holds exactly one field value from a set of possible fields. Unlike FlexRecord which holds all fields simultaneously, FlexEnum represents a choice among different alternatives.

import Data.FlexRecord (Field, FlexEnum)
import Data.FlexRecord (flexEnum)

type Status = FlexEnum
    [ Field "idle"    ()
    , Field "loading" Int
    , Field "error"   String
    ]

idleStatus :: Status
idleStatus = flexEnum @"idle" ()

loadingStatus :: Status
loadingStatus = flexEnum @"loading" 50

errorStatus :: Status
errorStatus = flexEnum @"error" "Connection failed"

Use flexEnum with type application to select which field you want to use. The value must match the field's type.

Pattern matching with flexMatch

flexMatch provides exhaustive pattern matching for FlexEnum values. Combined with inCase, you can handle all possible variants in a type-safe way:

import Data.FlexRecord (flexMatch, inCase)

status :: Status
status = flexEnum @"loading" 50

-- Exhaustive pattern matching using flexMatch and inCase
message :: String
message = flexMatch status
    $ inCase @"idle"    (\_   -> "Waiting...")
    . inCase @"loading" (\n  -> "Loading " ++ show n ++ "%")
    . inCase @"error"   (\err -> "Failed: " ++ err)
-- result: "Loading 50%"

The inCase function adds a handler for a specific field. Handlers are composed with . (just like field for records). Each handler receives the value contained in that variant and returns a result of the same type.

JSON

FlexRecord.Json provides ToJSON / FromJSON instances for FlexRecord.

import Data.Aeson (eitherDecode, encode)
import qualified Data.ByteString.Lazy.Char8 as LBS
import Data.FlexRecord (Field, FlexRecord, field, flexRecord, frGet)
import Data.FlexRecord.Json ()

Note: FlexRecord.Json is imported for side effects (instance declarations), so use import FlexRecord.Json ().

Simple usage

type Person =
    FlexRecord
    [ Field "name" String
    , Field "age" Int
    ]

person :: Person
person = flexRecord
    $ field @"name" "uwu"
    . field @"age" 18

Encoding:

encode person
-- {"name":"uwu","age":18}

Decoding:

case eitherDecode (LBS.pack "{\"name\":\"owo\",\"age\":20}") :: Either String Person of
    Left err -> putStrLn err
    Right p -> print (frGet @"name" p, frGet @"age" p)
-- ("owo",20)

About

extensible records with type-level fields

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors