Reputation: 13078
I was looking at the penultimate example in this blog post (also here), and after verifying it ran, it seemed to confirm that lens can generate Has typeclasses, which I take was the implication from the author of the blog. However, I miss where this is described, either in the lens contents or the lens tutorial. Any explanations external to official docs for how this is done would also be welcome. But it seems like this may just be standard when using the most basic feature (makeLenses
, or in this case, makeLensesWith
).
Here is the reproduced code:
#!/usr/bin/env stack
-- stack --resolver lts-8.12 script
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FunctionalDependencies #-}
import Control.Concurrent.Async.Lifted.Safe
import Control.Monad.Reader
import Control.Concurrent.STM
import Say
import Control.Lens
import Prelude hiding (log)
data Env = Env
{ envLog :: !(String -> IO ())
, envBalance :: !(TVar Int)
}
makeLensesWith camelCaseFields ''Env
modify :: (MonadReader env m, HasBalance env (TVar Int), MonadIO m)
=> (Int -> Int)
-> m ()
modify f = do
env <- ask
liftIO $ atomically $ modifyTVar' (env^.balance) f
logSomething :: (MonadReader env m, HasLog env (String -> IO ()), MonadIO m)
=> String
-> m ()
logSomething msg = do
env <- ask
liftIO $ (env^.log) msg
main :: IO ()
main = do
ref <- newTVarIO 4
let env = Env
{ envLog = sayString
, envBalance = ref
}
runReaderT
(concurrently
(modify (+ 1))
(logSomething "Increasing account balance"))
env
balance <- readTVarIO ref
sayString $ "Final balance: " ++ show balance
Upvotes: 3
Views: 118
Reputation: 3071
Field
is the word lens
uses to describe the pattern of one class per named field, allowing multiple records with the same field name but (optionally) different types. So camelCaseFields
, makeFieldOptics
, defaultFieldRules
all say in their name that they will generate these HasFoo
classes, in the usual terse style of lens
.
makeClassy
also generates classes named Has*
, but named after the data type, not the record field, and not following a different pattern.
Your code above generates the following code (shown with -ddump-splices
):
makeLensesWith camelCaseFields ''Env
======>
class HasBalance s a | s -> a where
balance :: Lens' s a
instance HasBalance Env (TVar Int) where
{-# INLINE balance #-}
balance f_a4eTr (Env x1_a4eTs x2_a4eTt)
= (fmap (\ y1_a4eTu -> (Env x1_a4eTs) y1_a4eTu)) (f_a4eTr x2_a4eTt)
class HasLog s a | s -> a where
log :: Lens' s a
instance HasLog Env (String -> IO ()) where
{-# INLINE log #-}
log f_a4eTx (Env x1_a4eTy x2_a4eTz)
= (fmap (\ y1_a4eTA -> (Env y1_a4eTA) x2_a4eTz)) (f_a4eTx x1_a4eTy)
Upvotes: 2