I have a Haskell type that uses record syntax.
data Foo a = Foo { getDims :: (Int, Int), getData :: [a] }
I don't want to export the Foo
value constructor, so that the user can't construct invalid objects. However, I would like to export getDims
, so that the user can get the dimensions of the data structure. If I do this
module Data.ModuleName(Foo(getDims)) where
then the user can use getDims
to get the dimensions, but the problem is that they can also use record update syntax to update the field.
getDims foo -- This is allowed (as intended)
foo { getDims = (999, 999) } -- But this is also allowed (not intended)
I would like to prevent the latter, as it would put the data in an invalid state. I realize that I could simply not use records.
data Foo a = Foo { getDims_ :: (Int, Int), getData :: [a] }
getDims :: Foo a -> (Int, Int)
getDims = getDims_
But this seems like a rather roundabout way to work around the problem. Is there a way to continue using record syntax while only exporting the record name for read access, not for write access?
Hiding the constructor and then defining new accessor functions for each field is a solution, but it can get tedious for records with a large number of fields.
Here's a solution with the new HasField
typeclass in GHC 8.2.1 that avoids having to define functions for each field.
The idea is to define an auxiliary newtype like this:
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE PolyKinds #-} -- Important, obscure errors happen without this.
import GHC.Records (HasField(..))
-- Do NOT export the actual constructor!
newtype Moat r = Moat r
-- Export this instead.
moat :: r -> Moat r
moat = Moat
-- If r has a field, Moat r also has that field
instance HasField s r v => HasField s (Moat r) v where
getField (Moat r) = getField @s r
Every field in a record r
will be accesible from Moat r
, with the following syntax:
λ :set -XDataKinds
λ :set -XTypeApplications
λ getField @"getDims" $ moat (Foo (5,5) ['s'])
(5,5)
The Foo
constructor should be hidden from clients. However, the field accessors for Foo
should not be hidden; they must be in scope for the HasField
instances of Moat
to kick in.
Every function in your public-facing api should return and receive Moat Foo
s instead of Foo
s.
To make the accessor syntax slightly less verbose, we can turn to OverloadedLabels
:
import GHC.OverloadedLabels
newtype Label r v = Label { field :: r -> v }
instance HasField l r v => IsLabel l (Label r v) where
fromLabel = Label (getField @l)
In ghci:
λ :set -XOverloadedLabels
λ field #getDims $ moat (Foo (5,5) ['s'])
(5,5)
Instead of hiding the Foo
constructor, another option would be to make Foo
completely public and define Moat
inside your library, hiding any Moat
constructors from clients.
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