4 ms·
> And substituting multiple different interfaces doesn't compose well (this is essentially the mtl problem). I think you're talking here about abstracting over
by dllthomas 2y ago
> And substituting multiple different interfaces doesn't compose well (this is essentially the mtl problem).
I think you're talking here about abstracting over the underlying monad, with a different implementation for testing than for production. That can work well[1], but it's not what I was talking about.
With my Writer (nb: io., not Control.Monad.) example, how does Haskell make it harder? I'm not talking about replacing the underlying monad, but to define an interface in terms of that underlying monad.
Fleshing it out, something like:
main :: IO ()
main = flip runReaderT () $ do
-- with Handle
writeStrings stdout
-- with IOVar
var <- liftIO $ newMVar ([] :: [ByteString])
writeStrings var
liftIO $ print =<< takeMVar var
writeStrings :: Writer w => w -> ReaderT r IO ()
writeStrings w = do
write w $ pack "oh hi\n"
write w $ pack "uh\n"
write w $ pack "bye then\n"
class Writer w where
write :: w -> ByteString -> ReaderT r IO ()
instance Writer Handle where
write w bs = liftIO $ hPut w bs
instance Writer (MVar [ByteString]) where
write w bs = liftIO $ modifyMVar_ w $ \ bss -> pure (bs:bss)
[1] A digression: Taking that approach, I think it's generally most comfortable to use a unique base type for each test (or natural clusters of them) and implement just the interfaces you need with an eye to the particular tests. The biggest problem I've seen is a tendency to push too much into that environment and thereby make too much implicit, which can make it confusing to distinguish and lead to things like the confused deputy problem.