12
votes

I have a record with fields of different types, and a function that is applicable to all of those types. As a small (silly) example:

data Rec = Rec  { flnum :: Float, intnum :: Int } deriving (Show)

Say, I want to define a function that adds two records per-field:

addR :: Rec -> Rec -> Rec
addR a b = Rec { flnum = (flnum a) + (flnum b), intnum = (intnum a) + (intnum b) }

Is there a way to express this without repeating the operation for every field (there may be many fields in the record)?

In reality, I have a record comprised exclusively of Maybe fields, and I want to combine the actual data with a record containing default values for some of the fields, to be used when the actual data was Nothing.

(I guess it should be possible with template haskell, but I am more interested in a "portable" implementation.)

4

4 Answers

8
votes

Yet another way is to use GHC.Generics:

{-# LANGUAGE FlexibleInstances, FlexibleContexts,
UndecidableInstances, DeriveGeneric, TypeOperators #-}

import GHC.Generics


class AddR a where
    addR :: a -> a -> a

instance (Generic a, GAddR (Rep a)) => AddR a where
    addR a b = to (from a `gaddR` from b)


class GAddR f where
    gaddR :: f a -> f a -> f a

instance GAddR a => GAddR (M1 i c a) where
    M1 a `gaddR` M1 b = M1 (a `gaddR` b)

instance (GAddR a, GAddR b) => GAddR (a :*: b) where
    (al :*: bl) `gaddR` (ar :*: br) = gaddR al ar :*: gaddR bl br

instance Num a => GAddR (K1 i a) where
    K1 a `gaddR` K1 b = K1 (a + b)


-- Usage
data Rec = Rec { flnum :: Float, intnum :: Int } deriving (Show, Generic)

t1 = Rec 1.0 2 `addR` Rec 3.0 4
7
votes

You can use gzipWithT for that.

I'm not an expert, so my version it a bit silly. It should be possible to call gzipWithT only once, e.g. using extQ and extT, but I failed to find the way to do that. Anyway, here is my version:

{-# LANGUAGE DeriveDataTypeable #-}

import Data.Generics

data Test = Test {
  test1 :: Int,
  test2 :: Float,
  test3 :: Int,
  test4 :: String,
  test5 :: String
  }
  deriving (Typeable, Data, Eq, Show)

t1 :: Test
t1 = Test 1 1.1 2 "t1" "t11"

t2 :: Test
t2 = Test 3 2.2 4 "t2" "t22"

merge :: Test -> Test -> Test
merge a b = let b' = gzipWithT mergeFloat a b
                b'' = gzipWithT mergeInt a b'
            in gzipWithT mergeString a b''

mergeInt :: (Data a, Data b) => a -> b -> b
mergeInt = mkQ (mkT (id :: Int -> Int)) (\a -> mkT (\b -> a + b :: Int))

mergeFloat :: (Data a, Data b) => a -> b -> b
mergeFloat = mkQ (mkT (id :: Float -> Float)) (\a -> mkT (\b -> a + b :: Float))

mergeString :: (Data a, Data b) => a -> b -> b
mergeString = mkQ (mkT (id :: String -> String)) (\a -> mkT (\b -> a ++ b :: String))

main :: IO ()
main = print $ merge t1 t2

Output:

Test {test1 = 4, test2 = 3.3000002, test3 = 6, test4 = "t1t2", test5 = "t11t22"}

The code is obscure, but the idea is simple, gzipWithT applies the specified generic function (mergeInt, mergeString, etc) to pair of corresponding fields.

3
votes

with vinyl (an "extensible records" package):

import Data.Vinyl
-- `vinyl` exports `Rec`

type Nums = Rec Identity [Float, Int]

which is equivalent to

data Nums' = Nums' (Identity Float) (Identity Int)

which is itself equivalent to

data Nums'' = Nums'' Float Int

then addR is simply

-- vinyl defines `recAdd`
addR :: Nums -> Nums -> Nums
addR = recAdd

and if you add a new field

type Nums = Rec Identity [Float, Int, Word]

you don't need to touch addR.

btw, recAdd is easy to define yourself, if you want to "lift" your own custom numeric operations, it's just

-- the `RecAll f rs Num` constraint means "each field satisfies `Num`"
recAdd :: RecAll f rs Num => Rec f rs -> Rec f rs -> Rec f rs
recAdd RNil RNil = RNil
recAdd (a :& as) (b :& bs) = (a + b) :& recAdd as bs

For convenience, you can define your own constructor:

nums :: Float -> Int -> Num
nums a b = Identity a :& Identity b :& RNil

and even a pattern for both constructing and matching values:

-- with `-XPatternSynonyms`
pattern Nums :: Float -> Int -> Num
pattern Nums a b = Identity a :& Identity b :& RNil

usage:

main = do
 let r1 = nums 1 2  
 let r2 = nums 3 4
 print $ r1 `addR` r2

 let (Nums a1 _) = r1
 print $ a1

 let r3 = i 5 :& i 6 :& i 7 :& z -- inferred
 print $ r1 `addR` (rcast r3) -- drop the last field

Since r3 is inferred as

(Num a, Num b, Num c) => Rec Identity [a, b, c]

you can (safely) upcast it to

rcast r3 :: (Num a, Num b) => Rec Identity [a, b]

you then specialize it

rcast r3 :: Nums

https://hackage.haskell.org/package/vinyl-0.5.2/docs/Data-Vinyl-Class-Method.html#v:recAdd

https://hackage.haskell.org/package/vinyl-0.5.2/docs/Data-Vinyl-Tutorial-Overview.html

2
votes

I don't think there's any way to do this, as to get the values from the fields, you need to specify their names, or pattern match on them - and similarly to set the fields, you specify their names, or use the regular constructor syntax to set them - where the syntax order matters.

Perhaps a slight simplification would be to use the regular constructor syntax and add a closure for the operation

addR' :: Rec -> Rec -> Rec
addR' a b = Rec (doAdd flnum) (doAdd intnum)
  where doAdd f = (f a) + (f b)

doAdd has the type (Num a) => (Rec -> a) -> a.

Additionally, if you plan on doing more than one operation on the record - for example, a subR, which does almost the same but subtracts - you can abstract away the behavior into a function by using RankNTypes.

{-# LANGUAGE RankNTypes #-}

data Rec = Rec  { flnum :: Float, intnum :: Int } deriving (Show)

opRecFields :: (forall a. (Num a) => a -> a -> a) -> Rec -> Rec -> Rec
opRecFields op a b = Rec (performOp flnum) (performOp intnum)
  where performOp f = (f a) `op` (f b)

addR = opRecFields (+)

subR = opRecFields (-)