Nice!
However, I would suggesting implementing this as a Bluefin library, rather than by hand (using s :> es => Eff es r
instead of LazyT s m r
). Why? A few reasons:
One reason is that Bluefin is a general theory of scopes, so some of the implementation is already written for you. Another reason is composability of your library: suppose I write
Scoped T1 s1 -> Scoped T2 s2 -> Lazy s1 (Lazy s2 m R1)
and you write
Scoped T1 s1 -> Scoped T2 s2 -> Lazy s2 (Lazy s1 m R2)
then how do we get (R1, R2)
? I’m not sure we can. In Bluefin those two can be written as
(s1 :> es, s2 :> es) => Scoped T1 s1 -> Scoped T2 s2 -> Eff es R1
(s1 :> es, s2 :> es) => Scoped T1 s1 -> Scoped T2 s2 -> Eff es R2
so they’re trivially compatible. Of course, similar typeclass machinery can be layered on top of LazyT
, but Bluefin’s already done it for you, so why bother?
Another reason is composibility with other effects. Bluefin provides Exception
, State
, Stream
etc. for you for free, so they’ll be available when you’re processing your lazy ByteString
s.
Furthermore, lazy-scope
typically requires MonadUnliftIO m
, and Bluefin’s Eff
is compatible with MonadUnliftIO
, so you don’t lose anything.
A couple of notes on your design:
unScope
is not safe because someone can write an NFData
instance that violates its invariants.
import Control.DeepSeq
import Lazy.Scope
import Relude hiding (withFile)
import System.IO (IOMode (ReadMode))
data Bad a = MkBad a deriving Show
instance NFData (Bad a) where rnf _ = ()
-- MkBad "*** Exception: /dev/zero: hGetBufSome: illegal operation (handle is closed)
--
-- HasCallStack backtrace:
-- ioException, called at libraries/ghc-internal/src/GHC/Internal/IO/Handle/Internals.hs:347:20 in ghc-internal:GHC.Internal.IO.Handle.Internals
bad :: IO (Bad LByteString)
bad =
withFile
"/dev/zero"
ReadMode
(\h -> do bs <- hGetContents h ; unScope (fmap MkBad bs))
- The
NFData
constraints on WithFile
are redundant.