pinchot-0.24.0.0: lib/Pinchot/SyntaxTree/Optics.hs
{-# LANGUAGE TemplateHaskell #-}
module Pinchot.SyntaxTree.Optics where
import Data.Data (Data)
import Data.List.NonEmpty (NonEmpty, toList)
import qualified Control.Lens as Lens
import qualified Language.Haskell.TH as T
import qualified Language.Haskell.TH.Syntax as Syntax
import Pinchot.Names
import Pinchot.Rules
import Pinchot.Types
-- | Creates optics declarations for a 'Rule', if optics can
-- be made for the 'Rule':
--
-- * 'Pinchot.terminal' gets a single 'Lens.Prism'
--
-- * 'Pinchot.nonTerminal' gets a 'Lens.Prism' for each constructor
--
-- * 'Pinchot.record' gets a single 'Lens.Lens'
--
-- * 'Pinchot.wrap', 'Pinchot.opt', 'Pinchot.star', and 'Pinchot.plus'
-- do not get optics. For those, you will typically want to use
-- 'Pinchot.wrappedInstances'.
--
-- Each rule in the sequence of 'Rule', as well as all ancestors of
-- those 'Rule's, will be handled.
--
-- Example: "Pinchot.Examples.RulesToOptics".
rulesToOptics
:: (Syntax.Lift t, Data t)
=> Qualifier
-- ^ Qualifier for module containing the data types that will get
-- optics
-> T.Name
-- ^ Type name for the terminal
-> [Rule t]
-> T.Q [T.Dec]
rulesToOptics qual termName
= fmap concat
. traverse (ruleToOptics qual termName)
. families
-- | Creates optics declarations for a single 'Rule', if optics can
-- be made for the 'Rule':
--
-- * 'Terminal' gets a single 'Lens.Prism'
--
-- * 'NonTerminal' gets a 'Lens.Prism' for each constructor
--
-- * 'Series' gets a single 'Lens.Prism'
--
-- * 'Record' gets a single 'Lens.Lens'
--
-- * 'Wrap', 'Opt', 'Star', and 'Plus' do not get optics.
ruleToOptics
:: (Syntax.Lift t, Data t)
=> Qualifier
-- ^ Qualifier for module containing the data type that will get
-- optics
-> T.Name
-- ^ Type name for the terminal
-> Rule t
-> T.Q [T.Dec]
ruleToOptics qual termName (Rule nm _ ty) = case ty of
Terminal pdct -> terminalToOptics qual termName nm pdct
NonTerminal bs -> sequence $ nonTerminalToOptics qual nm bs
Record sq -> sequence $ recordsToOptics qual nm sq
Series ne -> seriesToOptics qual termName nm ne
_ -> return []
-- | Creates a prism for a terminal type. Although a newtype wraps
-- each terminal, do not make a Wrapped or an Iso, because the
-- relationship between the outer type and the type that it wraps
-- typically is not isometric. Thus, use a Prism instead, which
-- captures this relationship properly.
terminalToOptics
:: Syntax.Lift t
=> Qualifier
-- ^ Qualifier for module containing the data type that will get
-- optics
-> T.Name
-- ^ Terminal type name
-> String
-- ^ Rule name
-> Predicate t
-> T.Q [T.Dec]
terminalToOptics qual termName nm (Predicate pdct) = do
ctorName <- lookupTypeName (quald qual nm)
e1 <- T.sigD (T.mkName ('_':nm))
$ T.forallT [ tyVarBndrA] (return [])
[t| Lens.Prism' ( $(T.conT termName), $(typeA) )
( $(T.conT ctorName) $(T.conT termName) $(typeA))
|]
e2 <- T.valD prismName (T.normalB expn) []
return [e1, e2]
where
prismName = T.varP (T.mkName ('_' : nm))
expn = do
x <- T.newName "_x"
ctorName <- lookupValueName (quald qual nm)
let fetchPat = T.conP ctorName [T.varP x]
fetchName = T.varE x
[| let fetch $fetchPat = $fetchName
store (term, a)
| $(fmap T.unType pdct) term
= Just ($(T.conE ctorName) (term, a))
| otherwise = Nothing
in Lens.prism' fetch store
|]
seriesToOptics
:: (Data t, Syntax.Lift t)
=> Qualifier
-- ^ Qualifier for module containing the data type that will get
-- optics
-> T.Name
-- ^ Terminal type name
-> String
-- ^ Rule name
-> NonEmpty t
-> T.Q [T.Dec]
seriesToOptics qual termName nm terminals = do
ctorName <- lookupTypeName (quald qual nm)
e1 <- T.sigD (T.mkName ('_':nm))
$ T.forallT [ tyVarBndrA] (return [])
[t| Lens.Prism' (NonEmpty ( $(T.conT termName), $(typeA) ))
( $(T.conT ctorName) $(T.conT termName) $(typeA))
|]
e2 <- T.valD prismName (T.normalB expn) []
return [e1, e2]
where
prismName = T.varP (T.mkName ('_' : nm))
expn = do
x <- T.newName "_x"
ctorName <- lookupValueName (quald qual nm)
let fetchPat = T.conP ctorName [T.varP x]
fetchName = T.varE x
[| let fetch $fetchPat = $fetchName
store terms
| fmap fst terms == $(Syntax.liftData terminals)
= Just ($(T.conE ctorName) terms )
| otherwise = Nothing
in Lens.prism' fetch store
|]
prismSignature
:: Qualifier
-> String
-- ^ Rule name
-> Branch t
-> T.DecQ
prismSignature qual nm (Branch inner rules) = do
ctorName <- lookupTypeName (quald qual nm)
T.sigD prismName
(forallA [t| Lens.Prism'
($(T.conT ctorName) $(typeT) $(typeA))
$(fieldsType) |])
where
prismName = T.mkName ('_' : inner)
fieldsType = case rules of
[] -> T.tupleT 0
Rule r1 _ _ : [] -> do
ctorName <- lookupTypeName (quald qual r1)
[t| $(T.conT ctorName) $(typeT) $(typeA) |]
rs -> foldl addType (T.tupleT (length rs)) rs
where
addType soFar (Rule r _ _) = do
ctorName <- lookupTypeName (quald qual r)
soFar `T.appT`
[t| $(T.conT ctorName) $(typeT) $(typeA) |]
setterPatAndExpn
:: Qualifier
-> BranchName
-> [a]
-- ^ List of rules
-> T.Q (T.PatQ, T.ExpQ)
setterPatAndExpn qual inner rules = do
names <- sequence . flip replicate (T.newName "_setterPatAndExpn")
. length $ rules
let pat = T.tupP . fmap T.varP $ names
expn = foldl addVar start names
where
start = do
ctorName <- lookupValueName (quald qual inner)
T.conE ctorName
addVar acc nm = acc `T.appE` (T.varE nm)
return (pat, expn)
prismSetter
:: Qualifier
-> Branch t
-> T.ExpQ
prismSetter qual (Branch inner rules) = do
(pat, expn) <- setterPatAndExpn qual inner rules
T.lamE [pat] expn
-- | Returns a pattern and expression to match a particular branch; if
-- there is a match, the expression will return each field, in a tuple
-- in a Right.
rightPatternAndExpression
:: Qualifier
-> BranchName
-> Int
-- ^ Number of fields
-> T.Q (T.PatQ, T.ExpQ)
rightPatternAndExpression qual inner n = do
names <- sequence . replicate n $ T.newName "_patternAndExpression"
ctorName <- lookupValueName (quald qual inner)
let pat = T.conP ctorName . fmap T.varP $ names
expn = T.appE (T.conE 'Right)
. T.tupE
. fmap T.varE
$ names
return (pat, expn)
-- | Returns a pattern and expression for branches that did not match.
-- Does not return anything if there are no other branches.
leftPatternAndExpression
:: [a]
-- ^ List of all other branches
-> Maybe (T.Q (T.PatQ, T.ExpQ))
leftPatternAndExpression ls
| null ls = Nothing
| otherwise = Just $ do
local <- T.newName "_leftPatternAndExpression"
return (T.varP local, T.appE (T.conE 'Left) (T.varE local))
prismGetter
:: Qualifier
-> Branch t
-- ^ Make prism for this branch
-> [Branch t]
-- ^ List of all branches
-> T.ExpQ
prismGetter qual (Branch inner rules) bs = do
local <- T.newName "_prismGetter"
(patCtor, bodyCtor) <- rightPatternAndExpression qual inner (length rules)
let firstElem = T.match patCtor (T.normalB bodyCtor) []
lastElem <- case leftPatternAndExpression bs of
Nothing -> return []
Just computation -> do
(patLeft, expLeft) <- computation
return [T.match patLeft (T.normalB expLeft) []]
T.lamE [T.varP local]
(T.caseE (T.varE local) $ firstElem : lastElem)
-- | Creates prisms for each 'Branch'.
nonTerminalToOptics
:: Qualifier
-- ^ Qualifier for module containing the data type that will get
-- optics
-> String
-- ^ Rule name
-> NonEmpty (Branch t)
-> [T.Q T.Dec]
nonTerminalToOptics qual nm bsSeq = concat $ fmap makePrism bs
where
bs = toList bsSeq
makePrism branch@(Branch inner _) =
[ prismSignature qual nm branch, binding ]
where
prismName = T.mkName ('_' : inner)
binding = T.valD (T.varP prismName) body []
where
body = T.normalB
$ (T.varE 'Lens.prism)
`T.appE` (prismSetter qual branch)
`T.appE` (prismGetter qual branch bs)
recordLensSignature
:: Qualifier
-> RuleName
-- ^ Name of the main rule
-> RuleName
-- ^ Name of the rule for this lens
-> Int
-- ^ Index for this lens
-> T.DecQ
recordLensSignature qual nm inner idx = do
ctorOuter <- lookupTypeName (quald qual nm)
ctorInner <- lookupTypeName (quald qual inner)
T.sigD lensName (forallA
[t| Lens.Lens' ($(T.conT ctorOuter) $(typeT) $(typeA))
($(T.conT ctorInner) $(typeT) $(typeA))
|])
where
lensName = T.mkName $ recordFieldName idx nm inner
recordLensGetter
:: Qualifier
-> String
-- ^ Record field name
-> T.ExpQ
recordLensGetter qual fieldNm = do
namedRec <- T.newName "_namedRec"
fieldNm <- lookupValueName $ quald qual ('_' : fieldNm)
let pat = T.varP namedRec
expn = (T.varE fieldNm)
`T.appE` (T.varE namedRec)
T.lamE [pat] expn
recordLensSetter
:: Qualifier
-> String
-- ^ Record field name
-> T.ExpQ
recordLensSetter qual fieldNm = do
namedRec <- T.newName "_namedRec"
namedNewVal <- T.newName "_namedNewVal"
fieldName <- lookupValueName (quald qual ('_' : fieldNm))
let patRec = T.varP namedRec
patNewVal = T.varP namedNewVal
expn = T.recUpdE (T.varE namedRec)
[ return (fieldName , T.VarE namedNewVal) ]
T.lamE [patRec, patNewVal] expn
recordLensFunction
:: Qualifier
-> RuleName
-- ^ Name of the main rule
-> RuleName
-- ^ Name of the rule for this lens
-> Int
-- ^ Index for this lens
-> T.DecQ
recordLensFunction qual nm inner idx =
let fieldNm = recordFieldName idx nm inner
lensName = T.mkName $ recordFieldName idx nm inner
getter = recordLensGetter qual fieldNm
setter = recordLensSetter qual fieldNm
body = (T.varE 'Lens.lens) `T.appE` getter `T.appE` setter
in T.funD lensName [T.clause [] (T.normalB body) []]
recordsToOptics
:: Qualifier
-- ^ Qualifier for module containing the data type that will get
-- optics
-> String
-- ^ Rule name
-> [Rule t]
-> [T.Q T.Dec]
recordsToOptics qual nm rules = do
let makeLens index (Rule inner _ _) = [ signature, function ]
where
signature = recordLensSignature qual nm inner index
function = recordLensFunction qual nm inner index
concat . zipWith makeLens [(0 :: Int) ..] $ rules
forallA :: T.TypeQ -> T.TypeQ
forallA = T.forallT [ tyVarBndrT, tyVarBndrA ] (return [])