pure-borrow-0.1.0.0: src/Control/Monad/Borrow/Pure/Experimental/Reborrowable.hs
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE ExplicitNamespaces #-}
{-# LANGUAGE ImpredicativeTypes #-}
{-# LANGUAGE PartialTypeSignatures #-}
{-# LANGUAGE QualifiedDo #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeAbstractions #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# OPTIONS_GHC -Wno-name-shadowing #-}
module Control.Monad.Borrow.Pure.Experimental.Reborrowable (
Reborrowable (..),
) where
import Control.Functor.Linear qualified as Control
import Control.Monad.Borrow.Pure.BO
import Control.Monad.Borrow.Pure.BO.Unsafe (reviveAlias)
import Data.Kind (Constraint, Type)
import Prelude.Linear
{- |
Borrow-like values that can be narrowed to a sublifetime and restored afterwards.
=== The obligation every method carries
An implementation of 'locally'', 'locally' or 'locally_' must not hand the
caller back the occurrence it was given. It must return it through a barrier
the optimizer cannot see through and that consumes the 'BO' state token —
'Control.Monad.Borrow.Pure.BO.Unsafe.reviveAlias' for a scalar borrow,
'Control.Monad.Borrow.Pure.Experimental.Borrows.reviveAliases' for a bundle.
This is not a performance convention. Reads that project a mutable header do
not go through the state token, so to GHC two of them on the same borrow
/variable/ are the same expression, and common-subexpression elimination is
entitled to serve the second from the first — across every write the scope
performed. A delimiter that returns its caller's own binder makes a post-scope
read syntactically identical to a pre-scope one; the result is a stale length
and a stale buffer, and writing through them runs off the end of the
allocation. See @Note [Restoring a borrow must break its Core identity]@ in
"Control.Monad.Borrow.Pure.BO.Internal" for the full argument, and treat it as
binding on any instance you write.
Each method is separately overridable, so each one owes this independently:
supplying a fast 'locally' while leaving 'locally'' to the default does not
discharge it for 'locally'.
-}
type Reborrowable :: (k -> Type) -> Constraint
class (bor ~ WithLifetime bor (LifetimeOf bor)) => Reborrowable bor where
type LifetimeOf bor :: Lifetime
type WithLifetime bor (α :: Lifetime) :: k -> Type
{- |
Executes an operation on a borrow in sub lifetime.
You may need @-XImpredicativeTypes@ extension to use this function.
Generalization of 'reborrowing'' and 'sharing'' that works for both 'Mut' and 'Share' borrows.
-}
locally' ::
bor a %1 ->
(forall β. WithLifetime bor (β /\ LifetimeOf bor) a %1 -> BO (β /\ α') (After β r)) %1 ->
BO α' (r, bor a)
{- |
The non-finalizing form, whose continuation returns its result directly.
This is a method rather than a function over 'locally'' so that an instance
can supply a delimiter that never builds an 'After' at all. The default is
the composition it replaces, so an existing instance keeps working and keeps
its current cost; @'Mut'@, @'Share'@ and
@'Control.Monad.Borrow.Pure.Experimental.Borrows.Muts'@ override it.
-}
locally ::
bor a %1 ->
(forall β. WithLifetime bor (β /\ LifetimeOf bor) a %1 -> BO (β /\ α') r) %1 ->
BO α' (r, bor a)
locally bor k = locally' bor \bor -> Control.pure Control.<$> k bor
{-# INLINE locally #-}
{- |
The result-discarding form.
The consumption of the result sits /in/ the returned value rather than being
sequenced at scope exit, so it runs when the caller forces the restored
borrow. That is deliberate: sequencing it at exit would make this stricter
than the implementation @+slow@ restores, and the two are required to stay
observationally equivalent. Linearity gives the restored borrow exactly one
holder, so any use of it forces the consumption exactly once and first.
-}
locally_ ::
(Consumable r) =>
bor a %1 ->
(forall β. WithLifetime bor (β /\ LifetimeOf bor) a %1 -> BO (β /\ α') r) %1 ->
BO α' (bor a)
locally_ bor k = uncurry lseq Control.<$> locally bor k
{-# INLINE locally_ #-}
instance Reborrowable (Mut α) where
type LifetimeOf (Mut α) = α
type WithLifetime (Mut α) β = Mut β
{-# SPECIALIZE instance Reborrowable (Mut α) #-}
locally' = reborrowing'
{-# INLINE locally' #-}
locally = reborrowing
{-# INLINE locally #-}
locally_ = reborrowing_
{-# INLINE locally_ #-}
instance Reborrowable (Share α) where
type LifetimeOf (Share α) = α
type WithLifetime (Share α) β = Share β
{-# SPECIALIZE instance Reborrowable (Share α) #-}
-- 'move' for a shared borrow is the identity, so @sh@ is the caller's own occurrence and returning it directly would carry the defect in Note [Restoring a borrow must break its Core identity].
-- Hand it back through 'reviveAlias' as the scalar delimiters do.
locally' shr k = Control.do
let %1 !(Ur sh) = move shr
r <- srunBO (k (upcast sh))
(r,) Control.<$> reviveAlias sh
{-# INLINE locally' #-}
-- The same, through the non-finalizing 'srunBO_', so that a continuation
-- which returns its result directly never builds an 'After' to discharge.
locally shr k = Control.do
let %1 !(Ur sh) = move shr
r <- srunBO_ (k (upcast sh))
(r,) Control.<$> reviveAlias sh
{-# INLINE locally #-}