What's the current state of record types and subtyping in Haskell?
I know there's been work done on things like overloaded record names, etc. Specifically, I'd like to make three different record types A
, B
, and C
where B
and C
contain all of the same field labels as A
, but do not share field labels with each other. Then, I'd like to be able to write functions where f : A -> int
, g: B -> int
, h: C -> int
where the function f
also accepts arguments with types B
and C
. Basically, I want B
and C
to be subtypes of A
. More specifically, it'd be nice if I didn't have to repeat all of the field labels. In pseudo code, this is something like
data A = A { a :: String }
data B = B { A, b :: Char }
data C = C { C, c :: Float }
f :: A/B/C -> int
g :: B -> int
h :: C -> int
Record syntax can be used with newtype with the restriction that there is exactly one constructor with exactly one field. The benefit here is the automatic creation of a function to unwrap the newtype. These fields are often named starting with run for monads, get for monoids, and un for other types.
In this example, two record accessors are defined, age and name, which allow us to access the age and name fields respectively. Record accessors are just Haskell functions which are automatically generated by the compiler. As such, they are used like ordinary Haskell functions.
As such, they are used like ordinary Haskell functions. By naming fields, we can also use the field labels in a number of other contexts in order to make our code more readable. lowerCaseName :: Person -> String lowerCaseName (Person { name = x }) = map toLower x
There is also special syntax for updating data types with field labels. a new value of type Person can be created by copying from alex, specifying which values to change: Record syntax can be used with newtype with the restriction that there is exactly one constructor with exactly one field.
There isn't one. There is a proposal to add overloaded records fields and a working implementation but AFAIK it hasn't been merged into GHC's head yet. You can read about the proposal here. Once it lands we'll have something similar to rho polymorphism but automagically generated/inferred type classes. Notice this isn't subtyping. {a :: Int, b :: Bool} <: {a :: Int}
isn't a concept in Haskell, instead we'd be able to say something like
foo :: r {a :: Int} -> Int
foo = a
which will really be something more like
foo :: Has "a" Int r => r -> Int
foo = a
If instead we wrote something like
foo :: {a :: Int} -> {a :: Int}
foo = id
And wanted this to behave like we had subtyping, we could do something like
foo _ = A {a = 1}
and return any type that's a subtype of {a :: Int}
.
There are alternatives as library, like Vinyl and to a certain extent, lens. I'd suggest investigating these for now especially if you want any compatibility with 7.6/7.8.
There are a few different ways of accomplishing this along with a few downsides.
Here are the types I'm working with:
data A = A { a :: String}
data B = B { bA :: A, b :: Char}
data C = C { cA :: A, c :: Float}
You can define f
as a method of some CanF
class that A
, B
, and C
all are instances of:
class CanF a where
f :: a -> Int
instance CanF A where
f = length . a
instance CanF B where
f = f . bA
instance CanF C where
f = f . cA
Defining B
and C
's instances in terms of A
's instance makes it clear that f
does the same thing in each case. It's easy to make f
do different things depending on which type defines the F
instance. The disadvantage to this approach is that any other f-like functions will need to be added as methods of the same "CanSomething" class.
main :: IO ()
main = do
print (f a)
print (f b)
print (f c)
where
a = A "Hello"
b = B a 'H'
c = C a 3.14
Another approach is to write f
as a function constrained by a class that always gives you an A
.
class RepA a where
getA :: a -> A
instance RepA A where
getA = id
instance RepA B where
getA = bA
instance RepA C where
getA = cA
f :: RepA a => a -> Int
f = length . a . getA
Here you have less flexibility for defining what f
can do, which might be good or bad. The advantage is that you can define other functions which work on A
's without adding new methods to your class.
My preferred way to handle this is the record-of-functions approach. Define a parameterized data type which holds functions you want to call. Then define specialized constructors for your record type. The disadvantage of this approach is that it is often more verbose. The advantage is that you can swap out behaviors by supplying a different F
to the f
function. Another advantage is that you can accomplish more without requiring language extensions.
data F a = F { f :: a -> Int }
af :: F A
af = F $ length . a
bf :: F B
bf = F $ f af . bA
cf :: F C
cf = F $ f af . cA
main :: IO ()
main = do
print (f af a)
print (f bf b)
print (f cf c)
where
a = A "Hello"
b = B a 'H'
c = C a 3.14
Since the HasField class is in GHC's head branch, with GHC's development version 8.2.0.20170310, we get a working example of polymorphism on records with specific fields, which can be used with handwritten structural subtyping:
{-# LANGUAGE DuplicateRecordFields, DataKinds, FlexibleContexts, TypeApplications #-}
import GHC.Records (HasField(getField))
data A = A { a :: String }
data B = B { a :: String, b :: Char }
data C = C { a :: String, c :: Float }
-- | length of field "a"
f :: HasField "a" rec String => rec -> Int
f = length . getField @"a"
main = do
print $ f $ A "a"
print $ f $ B "b" 'b'
print $ f $ C "c" 1.5
Documented in "GHC 8.2 branch" users guide (https://github.com/ghc/ghc/blob/ghc-8.2/docs/users_guide/glasgow_exts.rst) search for "Record field selector polymorphism"
GHC's 8.2.1 development binary from Herbert Riedel's Ubuntu PPA (https://launchpad.net/~hvr/+archive/ubuntu/ghc/+index?batch=150)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With