DanBurton / lens-family-th

Template Haskell to generate lenses for lens-family and lens-family-core
BSD 3-Clause "New" or "Revised" License
7 stars 4 forks source link

`makeEnum` to auto-generate `Traversal`s from enum types #10

Closed Gabriella439 closed 9 years ago

Gabriella439 commented 10 years ago

Would it be possible to define a makeEnum function that generates traversals from enum types? For example, if I had this code:

data Example = A | B | C

makeEnum ''Example

... it would generate these Traversals:

_A :: Traversal Example ()
_B :: Traversal Example ()
_C :: Traversal Example ()
DanBurton commented 10 years ago

If you can write the formula, then I can write the template Haskell.

At first glance I don't quite understand what those traversals are.

-- Dan Burton On Oct 5, 2014 3:52 PM, "Gabriel Gonzalez" notifications@github.com wrote:

Would it be possible to define a makeEnum function that generates traversals from enum types? For example, if I had this code:

data Example = A | B | C makeEnum ''Example

... it would generate these Traversals:

_A :: Traversal Example ()_B :: Traversal Example ()_C :: Traversal Example ()

— Reply to this email directly or view it on GitHub https://github.com/DanBurton/lens-family-th/issues/10.

Gabriella439 commented 10 years ago

Here's what the definition for _A would be:

_A :: Traversal' Example ()
_A k x = case x of
    A -> fmap (\() -> x) (k ())
    _ -> pure x

One of the use cases I have in mind is to use these in conjunction with the has combinator from lens, which you can think of as having this type:

has :: Traversal' a b -> a -> Bool

-- Example usage
has _A A = True
has _A B = False
has _A C = False

Also, if you add a Monoid instance for Constant, then you can even do things like:

instance Monoid m => Monoid (Constant m r) where
    mempty  = Constant mempty
    mappend (Constant m1) (Constant m2) = Constant (m1 <> m2)

-- Example usage
has (_A <> _B) A = True
has (_A <> _B) B = True
has (_A <> _B) C = False
DanBurton commented 10 years ago

Oh, okay. So as I understand it, this is just a slightly different version of the current makeTraversals (which currently only supports single-argument constructors). In fact, if we just generalize makeTraversals to handle "product" constructors and "no-argument" constructors, then we'd end up with something like:

data Example b c1 c2 = A | B b | C c1 c2

_A :: Traversal' (Example b c1 c2) ()
_B :: Traversal' (Example b c1 c2) b
_C :: Traversal' (Example b c1 c2) (c1, c2)

I've actually got a TODO in the code about this. I had quite forgotten about it. I should also document makeTraversals while I'm at it.

cTy <- case cTys of
    [t] -> return t
    -- TODO: this should be pretty easy to implement
     _ -> error $ "Traversal derivation not yet supported: "
    ++ "product constructor: " ++ nameBase cName

Most of what needs to be changed is deriveTraversalBody, which simply needs to be extended to handle the various cases. The logic is trivial; it's just a matter of slogging through the necessary TH. I'll probably get around to it sometime this week.

Now, if you're interested in generating type signatures, then it might make sense to split out "makeEnumTraversals", because that type signature is trivial. It's the signatures with type variables that can get complicated. I dunno, it might be easier than I think it is. But TH makes even the simplest tasks miserably tedious. I really wish Haskell had a better metaprogramming story.

*edit: stupid github markdown. Pardon the mess. /sigh

-- Dan Burton

On Mon, Oct 6, 2014 at 2:51 PM, Gabriel Gonzalez notifications@github.com wrote:

Here's what the definition for _A would be:

_A :: Traversal' Example ()A k x = case x of A -> fmap (() -> x) (k ()) -> pure x

One of the use cases I have in mind is to use these in conjunction with the has combinator from lens, which you can think of as having this type:

has :: Traversal' a b -> a -> Bool -- Example usagehas _A A = Truehas _A B = Falsehas _A C = False

Also, if you add a Monoid instance for Constant, then you can even do things like:

instance Monoid m => Monoid (Constant m r) where mempty = Constant mempty mappend (Constant m1) (Constant m2) = Constant (m1 <> m2) -- Example usagehas (_A <> _B) A = Truehas (_A <> _B) B = Truehas (_A <> _B) C = False

— Reply to this email directly or view it on GitHub https://github.com/DanBurton/lens-family-th/issues/10#issuecomment-58105861 .

Gabriella439 commented 10 years ago

Yeah, actually generalizing makeTraversals would work even better. Don't worry about the type signature for now. I've been writing those by hand anyway using type synonyms.

DanBurton commented 10 years ago

Okay I think I've done the generalization correctly. Clone the repo and open examples/traversal-test.hs in ghci to make sure it looks right to you.

The generated code is actually not as polymorphic as it could be.

data Opt b c d = A | B b | CD c d
$(makeTraversals ''Opt)

-- The generated code will look like this:
_A k A = (\ ~() -> A) `fmap` k ()
_A _ t = pure t

_B (B x) = (\ ~x' -> B x') `fmap` k x
_B _ t = pure t

_CD (CD x x2) = (\ ~(x',x'2) -> C x' x'2) `fmap` k (x, x2)
_CD _ t = pure t
ghci> :t _B
_B :: Applicative f => (t -> f t) -> Opt t c d -> f (Opt t c d)

The reason it isn't fully polymorphic is because of that "fallback" line, _A _ t = pure t. Because the t is reused, Haskell reuses its type. It doesn't realize that a pattern match has already failed and has thus given us (the knowledgeable programmers) additional type information about t in that branch of the function.

I have a vague idea of how to make it properly polymorphic as it should be, but I don't think I"ll bother doing it until someone asks. You only asked for the () case originally, which isn't polymorphic anyways, so I believe this much should suit your needs for now. :D

Confirm to me that it'll work for you and I"ll upload the new version to Hackage. I should note that this deserves a big fat "not well tested yet" disclaimer so I encourage you to test it out for yourself.

Gabriella439 commented 10 years ago

Yeah, I know what you mean about full polymorphism. To make it fully complete you'd have to add a type parameter for every field that you could pattern match against. I will give this a test drive in just a second.

Gabriella439 commented 10 years ago

I just tested it out and it works with flying colors. I exercised this on a large project to generate traversals both for constructors with no fields and constructors with multiple fields and everything still works perfectly.

DanBurton commented 9 years ago

I just published lens-family-0.4.0.0 to Hackage with this functionality.