Search code examples
haskellhaskell-lens

How to use an arbitrary makeFields lens argument with different types in the same function?


I am using makeFields from lens to generate fields overloaded for various structures. I would like to use these fields at one with multiple structures while having to state which field I want to use only once. It would look like this:

{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE FlexibleInstances #-}

import Control.Lens

data A = A
    { _aX :: String
    , _aY :: String
    }
makeFields ''A

data B = B
     { _bX :: String -> Char
     , _bY :: String -> Bool
     }
makeFields ''B

-- x can get _aX from an A and _bX from a B

a :: A
a = undefined

b :: B
b = undefined


q :: (Getter A String) AND (Getter B (String -> a)) -> a
q lens = (b^.lens) (a^.lens)

Which type should I give q? I tried letting GHC infer the types, but that failed.


Solution

  • To decide what is to be done, we need to know what the types of your (makeField-generated) fields are:

    GHCi> :t x
    x :: (HasX s a, Functor f) => (a -> f a) -> s -> f s
    

    So the abstraction covering all your x-bearing types (the abstraction I was whining about before noticing you were using makeFields) is a multi-parameter type class HasX, and similarly for the other fields. That gives us enough to use x with different types in a single implementation:

    -- Additional extension required: FlexibleContexts
    -- Note that GHC is able to infer this type.
    qx :: (HasX t (a -> b), HasX s a) => t -> s -> b
    qx t s = (t ^. x) (s ^. x)
    
    GHCi> import Data.Maybe
    GHCi> let testA = A "foo" "bar"
    GHCi> let testB = B (fromMaybe 'ø' . listToMaybe) null
    GHCi> qx testB testA
    'f'
    

    That, however, is not quite what you asked for. You wanted something like:

    q xOrY b a = (b^.xOrY) (a^.xOrY)
    

    Achieving that, however, requires abstracting over the classes HasX, HasY, etc. Doing so is, in fact, somewhat feasible thanks to the ConstraintKinds extension, as demonstrated in Could we abstract over type classes? Here it goes:

    -- Additional extensions required: ConstraintKinds, ScopedTypeVariables
    -- Additional import required: Data.Proxy
    -- GHC cannot infer this type.
    q :: forall h s t a b. (h t (a -> b), h s a) => Proxy a -> Proxy h
      -> (forall u c. h u c => Getting c u c) -> t -> s -> b
    q _ _ l t s =
        (t ^. (l :: Getting (a -> b) t (a -> b))) (s ^. (l :: Getting a s a))
    
    GHCi> q (Proxy :: Proxy String) (Proxy :: Proxy HasX) x testB testA
    'f'
    

    The first proxy, which determines the intermediate type, is necessary unless you give up this bit of generality and replace a by String. Additionally, you have to specify the field twice, both by passing the getter as an argument and through the second proxy. I am not at all convinced that this second solution is worth the trouble -- the extra boilerplate of having to define qx, qy, etc. looks quite a bit less painful than all the circuitousness involved here. Still, if any of you who are reading this would like to suggest an improvement, I'm all ears.