haskelltemplate-haskellhaskell-lens

Make Lenses (TH) with the Same Field Name using makeClassy


This question is regarding Edward A. Kmett's lens package (version 4.13)

I have a number of different data types all of which have a field that denotes the maximum number of elements contained (a business rule subject to run time change, not a collection implementation issue.) I would like to call this field capacity in all cases, but I quickly run into namespace conflicts.

I see in the lens documentation that there is a makeClassy template, but I cannot find documentation for it that I con understand. Will this template function allow me to have multiple lenses with the same field name?


EDITED: Let me add that I am quite capable of coding around the problem. I would like to know if makeClassy will solve the problem.


Solution

  • I found the documentation a bit unclear too; had to figure out what the various things Control.Lens.TH did by experimentation.

    What you want is makeFields:

    {-# LANGUAGE FunctionalDependencies
               , MultiParamTypeClasses
               , TemplateHaskell
      #-}
    
    module Foo
    where
    
    import Control.Lens
    
    data Foo
      = Foo { fooCapacity :: Int }
      deriving (Eq, Show)
    $(makeFields ''Foo)
    
    data Bar
      = Bar { barCapacity :: Double }
      deriving (Eq, Show)
    $(makeFields ''Bar)
    

    Then in ghci:

    *Foo
    λ let f = Foo 3
    |     b = Bar 7
    | 
    b :: Bar
    f :: Foo
    
    *Foo
    λ fooCapacity f
    3
    it :: Int
    
    *Foo
    λ barCapacity b
    7.0
    it :: Double
    
    *Foo
    λ f ^. capacity
    3
    it :: Int
    
    *Foo
    λ b ^. capacity
    7.0
    it :: Double
    
    λ :info HasCapacity 
    class HasCapacity s a | s -> a where
      capacity :: Lens' s a
        -- Defined at Foo.hs:14:3
    instance HasCapacity Foo Int -- Defined at Foo.hs:14:3
    instance HasCapacity Bar Double -- Defined at Foo.hs:19:3
    

    So what it's actually done is declared a class HasCapacity s a, where capacity is a Lens' from s to a (a is fixed once s is known). It figured out the name capacity by stripping off the (lowercased) name of the data type from the field; I find it pleasant not to have to use an underscore on either the field name or the lens name, since sometimes record syntax is actually what you want. You can use makeFieldsWith and the various lensRules to have some different options for calculating the lens names.

    In case it helps, using ghci -ddump-splices Foo.hs:

    [1 of 1] Compiling Foo              ( Foo.hs, interpreted )
    Foo.hs:14:3-18: Splicing declarations
        makeFields ''Foo
      ======>
        class HasCapacity s a | s -> a where
          capacity :: Lens' s a
        instance HasCapacity Foo Int where
          {-# INLINE capacity #-}
          capacity = iso (\ (Foo x_a7fG) -> x_a7fG) Foo
    Foo.hs:19:3-18: Splicing declarations
        makeFields ''Bar
      ======>
        instance HasCapacity Bar Double where
          {-# INLINE capacity #-}
          capacity = iso (\ (Bar x_a7ne) -> x_a7ne) Bar
    Ok, modules loaded: Foo.
    

    So the first splice made the class HasCapacity and added an instance for Foo; the second used the existing class and made an instance for Bar.

    This also works if you import the HasCapacity class from another module; makeFields can add more instances to the existing class and spread your types out across multiple modules. But if you use it again in another module where you haven't imported the class, it'll make a new class (with the same name), and you'll have two separate overloaded capacity lenses that are not compatible.


    makeClassy is a bit different. If I had:

    data Foo
      = Foo { _capacity :: Int }
      deriving (Eq, Show)
    $(makeClassy ''Foo)
    

    (noticing that makeClassy prefers you to have an underscore prefix on the fields, rather than the data type name)

    Then, again using -ddump-splices:

    [1 of 1] Compiling Foo              ( Foo.hs, interpreted )
    Foo.hs:14:3-18: Splicing declarations
        makeClassy ''Foo
      ======>
        class HasFoo c_a85j where
          foo :: Lens' c_a85j Foo
          capacity :: Lens' c_a85j Int
          {-# INLINE capacity #-}
          capacity = (.) foo capacity
        instance HasFoo Foo where
          {-# INLINE capacity #-}
          foo = id
          capacity = iso (\ (Foo x_a85k) -> x_a85k) Foo
    Ok, modules loaded: Foo.
    

    The class it's created is HasFoo, rather than HasCapacity; it's saying that anything from anything where you can get a Foo you can also get the capacity of the Foo. And the class hard-codes that the capacity is an Int, rather than overloading it as you had with makeFields. So this still works (because HasFoo Foo, where you just get the Foo by using id):

    *Foo
    λ let f = Foo 3
    | 
    f :: Foo
    
    *Foo
    λ f ^. capacity
    3
    it :: Int
    

    But you can't use this capacity lens to also get the capacity of an unrelated type.