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 ByteStrings.
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.