Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 912
John F. Miller Avatar asked Jan 05 '16 17:01

John F. Miller


2 Answers

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 "capcity" 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 splace made the class HasCapcity 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 HasCapcity 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 capcity 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 capcity lens to also get the capacity of an unrelated type.

like image 199
Ben Avatar answered Oct 13 '22 00:10

Ben


The templates are optional; you can always make your own classes and lenses.

class Capacitor s where
  capacitance :: Lens' s Int

Now any type with a capacity can be made an instance of this class.


An alternative approach is to factor out the capacity:

data Luggage a = Luggage { clothes :: a, capacity :: !Int }
like image 23
dfeuer Avatar answered Oct 12 '22 22:10

dfeuer