Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Record selectors in Haskell's Type Classes

I want to implement a Type Class with few default methods, but I'm getting an error, that I cannot use record selectors inside type classes definitions.

The following code basically creates type class which defines add function, which should add an element to the repr record of some data type. Here is the code:

import qualified Data.Graph.Inductive     as DG

class Graph gr a b where
    empty :: DG.Gr a b
    empty = DG.empty

    repr :: gr -> DG.Gr a b

    -- following function declaration does NOT work:
    add :: a -> gr -> gr
    add el g = g{repr = DG.insNode el $ repr g}

The compiler throws error:

repr is not a record selector
In the expression: g {repr = DG.insNode el $ repr g}
In an equation for add:
    add el g = g {repr = DG.insNode el $ repr g}

Is it possible to declare such methods in Haskell?

Clarification

I need such design because I've got some data types, which behave in simmilar way. Lets say, we got A, B and C data types. Each of them should have a record repr :: DG.Gr a b, where a and b are distinct for each of A, B and C.

A, B and C share the same functions, like add or delete (which basically add or delete elements to record repr). If these data types share a lot of functions it make sense to implement the functions in type class and make instances of this type class - these functions will be implemented for each of our data type automatically.

Additional I would love some of these data types (lets say I want B) to behave slighty different when calling add function on it. It is easy to implement this behaviour when making instance of the type class for B.

like image 309
Wojciech Danilo Avatar asked Oct 21 '22 05:10

Wojciech Danilo


1 Answers

  1. The record update syntax

     <record-instance> { <record-field-name> = ..., ... }
    

    works when <record-instance> is an instance/term of a known algebraic data type (so that <record-field-name> is it known field), in your code it is just some (ad-hoc) polymorphic parameter gr, so you need first to convert gr to Gr, then update it, and then...

  2. I think that gr and Gr should be equivalent in some sense, i.e. we need an inverse function for repr, say iface, to be able to implement add.

Here is an example:

{-# LANGUAGE MultiParamTypeClasses, TypeSynonymInstances, FlexibleInstances #-}

data Gr a b = Gr { _internal :: [(a, b)] } deriving ( Show, Read )

class Graph gr a b where

  repr :: gr -> Gr a b
  iface :: Gr a b -> gr

  -- iface . repr == id {gr}
  -- repr . iface == id {Gr a b}

  -- add element via "interface" (get a representation via @repr@, update it, and then 
  -- return an interface back with @iface@)
  add :: (a, b) -> gr -> gr
  add el g = let r = repr g in iface r { _internal = el : _internal r }
  -- or
  add el = iface . insNode el . repr where
    insNode x (Gr xs) = Gr (x : xs) -- or whatever

instance Graph String Int Int where
  repr = read
  iface = show

test :: String
test = add (1 :: Int, 2 :: Int) "Gr { _internal = [] }"
-- test => "Gr {_internal = [(1,2)]}"

If some data types A and B aggregate Gr a b (so that we can't write an inverse for repr), then we can do something like this:

{-# LANGUAGE MultiParamTypeClasses #-}

data Gr a b = Gr [(a, b)] deriving ( Show )

class Graph gr a b where

  repr :: gr -> Gr a b

  update :: gr -> (Gr a b -> Gr a b) -> gr
  -- 2: update :: gr -> Gr a b -> gr

  add :: (a, b) -> gr -> gr
  add el g = update g $ insNode el
    -- 2: update g (insNode el $ repr g)
    where insNode x (Gr xs) = Gr (x : xs)

data A = A { _aRepr :: Gr Char Char, _aRest :: Char } deriving ( Show )
data B = B { _bRepr :: Gr Int Int, _bRest :: Int } deriving ( Show )

instance Graph A Char Char where
  repr = _aRepr
  update r f = r { _aRepr = f $ _aRepr r }
  -- 2: update r g = r { _aRepr = g }

instance Graph B Int Int where
  repr = _bRepr
  update r f = r { _bRepr = f $ _bRepr r }
  -- 2: update r g = r { _bRepr = g }

testA :: A
testA = add ('1', '2') $ A (Gr []) '0'
-- => A {_aRepr = Gr [('1','2')], _aRest = '0'}

testB :: B
testB = add (1 :: Int, 2 :: Int) $ B (Gr []) 0
-- => B {_bRepr = Gr [(1,2)], _bRest = 0}

It is also possible to use lenses here:

{-# LANGUAGE MultiParamTypeClasses, TemplateHaskell #-}

import Control.Lens

data Gr a b = Gr [(a, b)] deriving ( Show )

insNode :: (a, b) -> Gr a b -> Gr a b
insNode x (Gr xs) = Gr (x : xs)

class Graph gr a b where
  reprLens :: Simple Lens gr (Gr a b)

add :: Graph gr a b => (a, b) -> gr -> gr
add el = reprLens %~ insNode el

data A = A { _aRepr :: Gr Char Char, _aRest :: Char } deriving ( Show )
data B = B { _bRepr :: Gr Int Int, _bRest :: Int } deriving ( Show )

makeLenses ''A
makeLenses ''B

instance Graph A Char Char where
  reprLens = aRepr

instance Graph B Int Int where
  reprLens = bRepr

main :: IO ()
main = do
  let a = A (Gr []) '0'
      b = B (Gr []) 0
  print $ add ('0', '1') a
  print $ add (0 :: Int, 1 :: Int) b
-- A {_aRepr = Gr [('0','1')], _aRest = '0'}
-- B {_bRepr = Gr [(0,1)], _bRest = 0}
like image 109
JJJ Avatar answered Oct 24 '22 12:10

JJJ