Open akhra opened 6 years ago
My reservation about the plain multi-param Has
class is that IMO error messages and type inference are a little bit worse. Overall, I've been trying to reduce my own usage of TH in favor of explicit, boilerplate-y code instead, at least when there's no chance of the boilerplate being buggy (as seems to be the case here). I'm not opposed to adding in some convenience functions though to do this. Want to take a crack at a PR? I'd be OK with both makeHasClass
and makeHasInstance
, or just one of them.
I've spent a solid day on this and gotten almost nowhere. My TH-foo is not strong. Not giving up, but it may be a while before I can try again. In case someone else wants to take a crack at it, I'll share my thoughts on design. I've been aiming for:
makeHasClass :: BaseName -> DecQ
makeHasInstance :: BaseName -> InstanceName -> FieldName -> DecQ
makeHasInstance
would be more versatile if it took a Lens' s a
instead of a field name, as in my initial example; but I'm not sure how to constrain the s
and a
, since it's only names in the signature otherwise. (Proxy
and extract the names later?)
Tangent: makeFieldLens
would be nice on its own, and would make a lensy makeHasInstance
even nicer.
A minor concern I have is that there is no configurability here; e.g. if someone doesn't want their instance members to end in L
, that's just too bad. I've thought about calling these derive*
, reserving make*
for a snazzier future implementation that takes a HasClassRules
of some sort. Not sure if it's really necessary though. Or, those could be make*With
.
Not a terrible stopgap:
module Control.Lens.TH.RIO where
import RIO
import qualified RIO.Char as C
import Control.Lens ((.~))
import Control.Lens.TH
import Language.Haskell.TH
makeRioLenses, makeRioClassy :: Name -> DecsQ
makeRioLenses = makeLensesWith rioLensRules
makeRioClassy = makeLensesWith rioClassyRules
rioLensRules, rioClassyRules :: LensRules
rioLensRules = lensRules
& lensField .~ rioFieldNamer
rioClassyRules = classyRules
& lensField .~ rioFieldNamer
& lensClass .~ rioClassyNamer
rioFieldNamer :: FieldNamer
rioFieldNamer _ _ name = case nameBase name of
x:xs -> [TopName (mkName $ C.toLower x:xs<>"L")]
_ -> []
rioClassyNamer :: ClassyNamer
rioClassyNamer name = case nameBase name of
n@(x:xs) -> Just (mkName $ "Has"<>n, mkName $ C.toLower x:xs<>"L")
[] -> Nothing
Hmm, this doesn't explode:
makeRioClassOnly = makeLensesWith rioClassOnlyRules
rioClassOnlyRules = classyRules
& lensField .~ (\_ _ _ -> [])
& lensClass .~ rioClassyNamer
Gonna play with this a bit more. I think rioFieldNamer
may be a mistake overall, since it can generate names which overlap with HasFoo
typeclass members per rioClassyNamer
. But makeRioClassOnly
seems, at first blush, to be equivalent to makeHasClass
as proposed.
It's been a while, but I stumbled across this - the rules look pretty good, but if I understand correctly one problem with them is that they don't produce the superclass constraints. Not sure if that's something that can be done with lens rules.
I made a prototype that correctly generates instances for superclasses (though it doesn't support types with type parameters at the moment).
First, a usage example
data WindowSize = MkWindowSize { windowWidth :: !Natural
, windowHeight :: !Natural
}
makeRioClassy ''WindowSize
-- This will generate
-- class (HasWindowWidth env, HasWindowHeight env)
-- => HasWindowSize env where windowSizeL :: Lens' env WindowSize
-- class HasWindowWidth env where ... (method signatures omitted for brevity from here on out)
-- class HasWindowHeight env
-- (as well as associated `id` instances, omitted for brevity for this and the other types)
newtype Email = MkEmail { emailString :: String }
makeRioClassy ''Email
-- This will generate
-- class HasEmailString env => HasEmail env
-- class HasEmailString env
data Config = MkConfig { windowSize :: !WindowSize
, verbose :: !Bool
, studentEmail :: !Email
, teacherEmail :: !Email
}
makeRioClassy ''Config
-- This will *not* generate class HasWindowSize, since that class already exists
-- However, it *will* generate
-- instance HasWindowSize Config -- as well as instances for HasWindowWidth and HasWindowHeight
-- class HasVerbose env
-- class HasStudentEmail env -- since generation is name-driven it will *not* generate a HasEmail instance
-- class HasTeacherEmail env -- ditto
-- class (HasWindowSize env, HasVerbose env, HasStudentEmail env, HasTeacherEmail env)
-- => HasConfig env
data App = MkApp { config :: !Config
, logFunc :: !LogFunc
}
makeRioClassy ''App
-- This will generate
-- class (HasConfig env, HasLogFunc env) => HasApp env
-- as well as
-- instance HasConfig App, instance HasWindowSize App, instance HasWindowWidth App, ...
-- instance HasLogFunc App
The full output and the implementation of makeRioClassy
can be found in this gist.
Considering the first bit of the
Has
-pattern example in the current docs:This seems awfully expository. Using
Data.Has
shaves it down to this (instance Has a a
is provided):Is there a particular reason Rio doesn't already include
Has
? (I can think of a few possibilities, but haven't seen any specific comments.) If so, how do we feel about adding some Template Haskell to generate the class and identity instance from a type? Then we could reduce it to something like:(Edit:
makeHasInstance
might not be worthwhile, butmakeHasClass
certainly seems so in absence of genericHas
.)