0

given this piece of code:

{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FunctionalDependencies #-}

module Foo where

import Control.Lens ((^.), makeFieldsNoPrefix)
import Prelude hiding (id)

data Bar
  = Bar1 { _id :: Int
         , _name :: String
         }
  | Bar2 { _name :: String }

$(makeFieldsNoPrefix ''Bar)


data Foo = Foo { _id :: Int
               , _name :: String
               }

$(makeFieldsNoPrefix ''Foo)

a = (undefined :: Foo) ^. name -- compiles fine

b = (undefined :: Foo) ^. id -- doesnt compile
{-
    • No instance for (Monoid Int) arising from a use of ‘id’
    • In the second argument of ‘(^.)’, namely ‘id’
      In the expression: (undefined :: Foo) ^. id
      In an equation for ‘b’: b = (undefined :: Foo) ^. id
-}

As far as I can understand, it seems id needs an instance of Monoid because of Bar that could fail when the instance is of type Bar2 (which doesnt have an id field).

But since I'm working on a Foo (which always has an id field) it should work, doesnt it ?

I know I could solve this with prefixing the field with the class name like:

data Foo = Foo { _fooId :: Int, _fooName :: String }

but if there is a nice solution without cluttering my fields name then I'm all for it :-)

itsu
  • 222
  • 1
  • 10
  • 4
    Uff, there are at least three things here that I'd consider generally a bad idea: **1.** making record fields for sum types. (Lenses do to some degree solve the problems that come with that, but as you've encountered this brings its own problems.) **2.** Duplicate record fields. (Always were a half-baked concept; only `OverloadedLabels` has made sense of this.) **3.** Don't re-use standard names for something different, in particular not for something as fundamental as `id`. – leftaroundabout Nov 29 '19 at 11:19
  • Yeah, I know about 1 and 2 but I thought lens would solve their issue :-( – itsu Nov 29 '19 at 12:33
  • Is it sure that **1** is a bad idea when used with lens ? It seems strange to me that the first example on http://hackage.haskell.org/package/lens-4.18.1/docs/Control-Lens.html is using sum types of records if it was a bad practice – itsu Nov 29 '19 at 12:55
  • 1
    Well, **I** consider it a bad idea. Opinions may vary. – leftaroundabout Nov 29 '19 at 13:12
  • You can get around a lot of these issues with [generic-lens](https://hackage.haskell.org/package/generic-lens). And as a bonus, no TH splices! – Carl Nov 29 '19 at 17:31

1 Answers1

4

So, the issue here is caused in part by the record used in a sum-type. We can generate a lens for name for Foo since each constructor has a name field; but only one constructor has an id. This causes TemplateHaskell to generate the following HasId class, which you can see for yourself by running :browse Foo in ghci:

class HasId s a | s -> a where
  id :: Traversal' s a

As you can see, it set the type to Traversal'. Since there can only be one HasId typeclass, when you makeFields on Foo it'll re-use the same typeclass, and even though it CAN generate a lens, the typeclass method id only has type Traversal, and you need a Monoid to use ^. on a traversal.

If you swap the ordering of your makeFields calls in the module you'll notice the typeclass is now generated with id as a Lens; but now the second makeLenses call fails to compile because it's generating a Traversal, not a lens.

To sum up the problem, you're expecting the typeclass's id method to change types based on how it's used (by picking either a Lens or Traversal), but that's not how typeclasses work in Haskell.

You have a few options here, but you really need to decide on what you want the semantics to be. The safest is to stick with what you've got here, (use the Traversal) and always use preview a.k.a. (^?) to access the id field. Alternatively you could generate separate combinators; a fooId and a barId, one a lens and the other a Traversal. Alternatively you could implement the HasId typeclass manually, and provide a 'default' id for when it's missing (or call error), but both of these would result in an unlawful lens.

You could do something really gross and determine whether each type's id field is a Lens or Traversal using Type families or something like that; but it's going to be unidiomatic and really hard to understand. It's more work than it's worth.

The crux of it is, you can't have a valid lens for id if there's a constructor fo the type that doesn't have an id. It's up to you how you want to handle that case.

Chris Penner
  • 1,881
  • 11
  • 15
  • Such a well detailed response, this is great. Thank you so much !! I chose to use the lens lib because I thought it'd solved the haskell record issue but I guess the easiest solution is still to prefix my record with the class name :-( – itsu Dec 01 '19 at 12:21
  • 1
    No problem :) If you're interested in learning more about lenses I'm releasing a comprehensive lenses book later this month! https://leanpub.com/optics-by-example/ – Chris Penner Dec 01 '19 at 16:31