miso-1.12.0.0: src/Miso/Lens/TH.hs
-----------------------------------------------------------------------------
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE TemplateHaskellQuotes #-}
-----------------------------------------------------------------------------
-- |
-- Module : Miso.Lens.TH
-- Copyright : (C) 2016-2026 David M. Johnson
-- License : BSD3-style (see the file LICENSE)
-- Maintainer : David M. Johnson <code@dmj.io>
-- Stability : experimental
-- Portability : non-portable
--
-- = Overview
--
-- "Miso.Lens.TH" generates 'Lens' definitions via Template Haskell, similar
-- to @lens@'s @makeLenses@ and @makeClassy@. Fields must be prefixed with
-- @_@ — the generated lens name is the field name with the underscore dropped.
--
-- Enable the extension and splice at the declaration level:
--
-- @
-- {-\# LANGUAGE TemplateHaskell \#-}
-- import "Miso.Lens.TH" ('makeLenses', 'makeClassy')
-- @
--
-- = makeLenses
--
-- Generates a @'Lens' Record Field@ for each @_@-prefixed record field:
--
-- @
-- data Model = Model
-- { _count :: Int
-- , _text :: 'Miso.String.MisoString'
-- } deriving (Eq)
--
-- 'makeLenses' \'\'Model
-- -- Generates:
-- -- count :: 'Lens' Model Int
-- -- text :: 'Lens' Model 'Miso.String.MisoString'
--
-- update :: Action -> 'Miso.Effect.Effect' p props Model Action
-- update Increment = count '+=' 1
-- update (SetText t) = text '.=' t
-- @
--
-- = makeClassy
--
-- Generates a @HasFoo@ typeclass with a self-lens @foo :: Lens s Foo@ and
-- one lens per @_@-prefixed field. This enables lens composition across
-- record types that embed @Foo@:
--
-- @
-- data Foo = Foo { _fooX :: Int, _fooY :: Int }
-- 'makeClassy' \'\'Foo
-- -- Generates:
-- -- class HasFoo s where
-- -- foo :: 'Lens' s Foo
-- -- fooX :: 'Lens' s Int -- fooX = foo . lens _fooX ...
-- -- fooY :: 'Lens' s Int
-- -- instance HasFoo Foo where foo = 'this'
--
-- data Bar = Bar { _barFoo :: Foo, _barZ :: Double }
-- 'makeLenses' \'\'Bar
-- instance HasFoo Bar where foo = barFoo
-- -- Now barX :: 'Lens' Bar Int (via foo composition)
-- @
--
-- = Comparison with Generic approach
--
-- [Template Haskell] "Miso.Lens.TH" — @_@ prefix required; explicit splice
-- [Overloaded labels] "Miso.Lens.Generic" — no TH; derives via @Generic@
--
-- = See also
--
-- * "Miso.Lens" — 'Miso.Lens.Lens', 'Miso.Lens.lens', 'Miso.Lens.view',
-- 'Miso.Lens.set', and the update operators ('.=', '+=', '%=', …)
-- * "Miso.Lens.Generic" — label-based alternative requiring no TH splice
-----------------------------------------------------------------------------
module Miso.Lens.TH
( -- ** TH
makeLenses
, makeClassy
-- ** Re-exports
, lens
, compose
, this
, Lens
) where
-----------------------------------------------------------------------------
import Data.Char
import Data.Maybe
import Language.Haskell.TH
-----------------------------------------------------------------------------
import Miso.Util (compose)
import Miso.Lens (this, lens, Lens)
-----------------------------------------------------------------------------
-- | Automatically generates Haskell lenses via template-haskell.
--
makeLenses
:: Name
-- ^ The name of the record type to derive lenses for (e.g. @\'\'MyModel@)
-> Q [Dec]
makeLenses name = do
reify name >>= \case
TyConI (NewtypeD _ _ _ _ con _) -> do
case con of
RecC _ fieldNames ->
pure (processFieldNames fieldNames)
_ -> pure []
TyConI (DataD _ _ _ _ cons _) ->
flip concatMapM cons $ \case
RecC _ fieldNames -> do
pure (processFieldNames fieldNames)
_ -> pure []
_ -> pure []
where
processFieldNames fieldNames = concat
[ mkFields fieldName (ConT name) fieldType
| (fieldName, _, fieldType) <- fieldNames
, listToMaybe (nameBase fieldName) == Just '_'
]
mkFields fieldName conType fieldType =
let -- dmj: drops '_' prefix
lensName = mkName (drop 1 (nameBase fieldName))
in
[ FunD lensName
[ Clause [] (NormalB (mkLens fieldName)) []
]
, SigD lensName (mkLensType conType fieldType)
]
concatMapM f xs =
concat <$> mapM f xs
mkLensType conType =
AppT (AppT (ConT ''Lens) conType)
mkLens fieldName =
AppE (AppE (VarE 'lens) (VarE fieldName))
$ LamE [ VarP recName, VarP fieldVar ]
$ RecUpdE (VarE recName) [ (fieldName, VarE fieldVar) ]
where
recName = mkName "record"
fieldVar = mkName "field"
-----------------------------------------------------------------------------
-- | Automatically generates classy lenses via template-haskell.
makeClassy
:: Name
-- ^ The name of the record type to derive a @Has@ typeclass and lenses for (e.g. @\'\'MyModel@)
-> Q [Dec]
makeClassy name = do
reify name >>= \case
TyConI (NewtypeD _ _ _ _ con _) -> do
case con of
RecC _ fieldNames ->
pure (processFieldNames fieldNames)
_ -> pure []
TyConI (DataD _ _ _ _ cons _) ->
flip concatMapM cons $ \case
RecC _ fieldNames -> do
pure (processFieldNames fieldNames)
_ -> pure []
_ -> pure []
where
instanceName =
AppT (ConT (mkName ("Has" <> baseName))) (ConT name)
baseName = nameBase name
baseNameLower
| x : xs <- baseName = toLower x : xs
| otherwise = []
processFieldNames fieldNames =
[ InstanceD Nothing [] instanceName
[ ValD (VarP (mkName baseNameLower)) (NormalB (VarE 'this)) []
-- instance HasFoo Foo where foo = this
]
, ClassD [] (mkName $ "Has" <> nameBase name)
[ PlainTV (mkName baseNameLower) BndrReq
] [] $ reverse $ concat
[ mkFields fieldName (VarT (mkName baseNameLower)) fieldType
| (fieldName, _, fieldType) <- fieldNames
, listToMaybe (nameBase fieldName) == Just '_'
] ++
[ SigD
(mkName baseNameLower)
(AppT
(AppT
(ConT ''Lens)
(VarT (mkName baseNameLower)))
(ConT name))
]
]
mkFields fieldName varType fieldType =
let -- dmj: drops '_' prefix
lensName = mkName (drop 1 (nameBase fieldName))
in
[ FunD lensName
[ Clause [] (NormalB (wrapMkLens fieldName)) []
]
-- fooX = lens _fooX (\r x -> r { _fooX = x }) . foo
, SigD lensName (mkLensType varType fieldType)
-- fooY :: Lens foo Int
]
concatMapM f xs =
concat <$> mapM f xs
mkLensType varType x =
AppT (AppT (ConT ''Lens) varType) x
wrapMkLens fieldName =
AppE (AppE (VarE 'compose) (mkLens fieldName)) (VarE (mkName baseNameLower))
mkLens fieldName
= AppE (AppE (VarE 'lens) (VarE fieldName))
$ LamE [ VarP recName, VarP fieldVar ]
$ RecUpdE (VarE recName) [ (fieldName, VarE fieldVar) ]
where
recName = mkName "record"
fieldVar = mkName "field"
-------------------------------------------------------------------------------