Search code examples
haskellalgebraic-data-types

A more succinct way to map functions onto fields of an algebraic datatype?


I have a datatype with lots of fields:

data ManyFields a b c d .. = MF { f1 :: a, f2 :: b, f3 :: c .. }

Problem One

How do I map function onto each field while avoiding implementing a map function for each one. For example, this looks very tedious and non-idiomatic:

-- | Note I am explicitly constructing ManyField after mapping a function onto the field
-- | This looks bad
mapf1 :: (a -> a1) -> ManyFields a b c ..  -> ManyFields a1 b c ..
mapf1 g mf = MF (g . f1 $ mf) (f2 mf) ..

-- | Repeat for each field
mapf2 :: (b -> b1) -> ManyFields a b c .. -> ManyFields a b1 c ...

I think some sort of higher-order function that abstract out the pattern of constructor . (mapfunction f) would cut down on the boilerplate, but is there a better solution?

Problem Two

If I want to zip many ManyFields together and map a function of arbitrary arity onto each field, does this sound like it could be an instance of some type class?

Use case:

(==) `mapFunction` mf1 `pairWiseZipField` mf2 

It kind of looks like an applicative to me, but again I'm not sure how to implement fmap onto this type.


Solution

  • You can't really do this with a standard function. The best approach is to have a map function for each field. Happily, you can generate these automatically with a bit of template haskell from the lens library. It would look something like this:

    data ManyFields a b c d = MF { _f1 :: a, _f2 :: b, _f3 :: c, _f4 :: d }
    
    makeLenses ''ManyFields
    

    This generates a lens for each field of ManyFields. The lens is a simple construct that allows you to both access and change the value there--the changes can even be polymorphic, just like map! Note how each field is prefixed with an underscore: the lens has the same name as the field minus the underscore.

    You can now access values like this:

    > foo = MF 'a' "b" 3 False
    > foo^.f1
    'a'
    

    You can set values using the set operator. When used with a lens, it creates a setter function:

    > :t set f1
    set f1 :: a' -> ManyFields a b c d -> ManyFields a' b c d
    

    To actually use it, you could do this:

    > set f1 () foo
    MF () "b" 3 False
    

    Since you have a getter and a setter, writing a map function is pretty trivial. Happily, we don't even have to do this: the library provides a function called over:

    > :t over f1 
    over f1 :: (a -> a') -> ManyFields a b c d -> ManyFields a' b c d
    

    If you like infix operators more, set can also be called .~ and over can be called %~. (The latter has a mnemonic: % is mod, or "modify" :P.) This is also useful with the & operator which is just $ flipped. So the following two versions are equal:

    > over f1 ord foo
    MF 97 "b" 3 False
    > foo & f1 %~ ord
    MF 97 "b" 3 False
    

    I personally think the operators are a bit much. Unless you're going to be using lenses everywhere, I would stick to set and over.

    I don't know of a good solution for zipping functions the way you described. But do take a look through the rest of the lens library--it's pretty large, and you never know what you'll find!