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.)
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. usingextQ
andextT
, but I failed to find the way to do that. Anyway, here is my version:Output:
The code is obscure, but the idea is simple,
gzipWithT
applies the specified generic function (mergeInt
,mergeString
, etc) to pair of corresponding fields.with
vinyl
(an "extensible records" package):which is equivalent to
which is itself equivalent to
then
addR
is simplyand if you add a new field
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 justFor convenience, you can define your own constructor:
and even a pattern for both constructing and matching values:
usage:
Since
r3
is inferred asyou can (safely) upcast it to
you then specialize it
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
Yet another way is to use GHC.Generics:
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
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 usingRankNTypes
.