Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Haskell Export Record for Read Access Only

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?

like image 995
Silvio Mayolo Avatar asked Aug 12 '17 23:08

Silvio Mayolo


1 Answers

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 Foos instead of Foos.

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.

like image 195
danidiaz Avatar answered Oct 14 '22 00:10

danidiaz