HORIZON HASKELLDocslts/ghc-9.14.x45dd5372026-10-11Search names, modules, packages, or :: a typeCtrl K

GHC 9.14.1 · lts/ghc-9.14.x · 45dd537 · 2026-10-11

Moduleeffectful-core-2.7.1.2GHC2021

Effectful.Internal.Unlift

Implementation of sequential and concurrent unlifts.

This module is intended for internal use only, and may change without warning in subsequent releases.

  • 3 types
  • 6 values

Unlifting strategies

3 declarations
datadata UnliftStrategy
#

The strategy to use when unlifting Eff computations via withEffToIO or the localUnlift family.

Constructors

  • SeqUnlift

    The sequential strategy is the fastest and a default setting for IOE. Any attempt of calling the unlifting function in threads distinct from its creator will result in a runtime error.

  • SeqForkUnlift

    Like SeqUnlift, but all unlifted actions will be executed in a cloned environment.

    The main consequence is that thread local state is forked at the point of creation of the unlifting function and its modifications in unlifted actions will not affect the main thread of execution (and vice versa):

    Example3 expressions
    import Effectfulimport Effectful.State.Dynamic:{ action :: (IOE :> es, State Int :> es) => Eff es () action = do   modify @Int (+1)   withEffToIO SeqForkUnlift $ \unlift -> unlift $ modify @Int (+2)   modify @Int (+4):}
    Example1 expression
    runEff . execStateLocal @Int 0 $ action5
    Example1 expression
    runEff . execStateShared @Int 0 $ action7

    Because of this it's possible to safely use the unlifting function outside of the scope of effects it captures, e.g. by creating an IO action that executes effectful operations and running it later:

    Example1 expression
    :{  delayed :: UnliftStrategy -> IO (IO String)  delayed strategy = runEff . evalStateLocal "Hey" $ do    r <- withEffToIO strategy $ \unlift -> pure $ unlift get    modify (++ "!!!")    pure r:}

    This doesn't work with the SeqUnlift strategy because when the returned action runs, State is no longer in scope:

    Example1 expression
    join $ delayed SeqUnlift*** Exception: version (...) /= storageVersion (0)...

    However, it does with the SeqForkUnlift strategy:

    Example1 expression
    join $ delayed SeqForkUnlift"Hey"
  • ConcUnlift !Persistence !Limit

    The concurrent strategy makes it possible for the unlifting function to be called in threads distinct from its creator. See Persistence and Limit settings for more information.

Instances5Eq, Ord, Show, Generic, Rep
datadata Persistence
#

Persistence setting for the ConcUnlift strategy.

Different functions require different persistence strategies. Examples:

  • Lifting pooledMapConcurrentlyN from the unliftio library requires the Ephemeral strategy as we don't want jobs to share environment changes made by previous jobs run in the same worker thread.

  • Lifting forkIOWithUnmask requires the Persistent strategy, otherwise the unmasking function would start with a fresh environment each time it's called.

Both cases come down to what happens when the unlifting function is called more than once in the same thread. If a thread calls it only once, the Persistence setting makes no observable difference.

Example 1

Consider a thread that modifies thread local state, then inspects it with a second call to the unlifting function:

Example4 expressions
import Control.Concurrentimport Control.Monadimport Effectfulimport Effectful.State.Dynamic
Example1 expression
:{  modifyThenGet :: UnliftStrategy -> IO Int  modifyThenGet strategy = runEff . evalStateLocal @Int 0 $ do    withEffToIO strategy $ \unlift -> do      result <- newEmptyMVar      void . forkIO $ do        unlift $ modify @Int (+1)        putMVar result =<< unlift (get @Int)      takeMVar result:}

With the Persistent strategy the unlifting function keeps the environment between the calls, so the second call sees the modification from the first one:

Example1 expression
modifyThenGet $ ConcUnlift Persistent (Limited 1)1

On the other hand, with Ephemeral each call to the unlifting function starts with a fresh copy of the environment, so the modification is silently lost:

Example1 expression
modifyThenGet $ ConcUnlift Ephemeral (Limited 2)0

This also showcases the limit meaning different things for the two settings: for the Persistent strategy it limits the number of threads the unlifting can happen in, for Ephemeral it limits the number of calls to the unlifting function.

Example 2

Consider a situation where a single worker thread runs multiple independent jobs:

Example1 expression
:{  twoJobs :: UnliftStrategy -> IO [Int]  twoJobs strategy = runEff . evalStateLocal @Int 0 $ do    withEffToIO strategy $ \unlift -> do      result <- newEmptyMVar      void . forkIO $ do        let job = unlift $ modify @Int (+1) >> get @Int        putMVar result =<< sequence [job, job]      takeMVar result:}

With Ephemeral both jobs start from the environment as it was when the unlifting function was created:

Example1 expression
twoJobs $ ConcUnlift Ephemeral Unlimited[1,1]

With Persistent the second job inherits changes made by the first one, even though the user would most likely expect them to be independent:

Example1 expression
twoJobs $ ConcUnlift Persistent Unlimited[1,2]

Constructors

  • Ephemeral

    Don't persist the environment between calls to the unlifting function in threads distinct from its creator.

  • Persistent

    Persist the environment between calls to the unlifting function within a particular thread.

Instances5Eq, Ord, Show, Generic, Rep
datadata Limit
#

Limit setting for the ConcUnlift strategy.

Constructors

  • Limited !Int

    Behavior dependent on the Persistence setting.

    For Ephemeral, it limits the amount of uses of the unlifting function in threads distinct from its creator to N. The unlifting function will create N copies of the environment when called N times and K+1 copies when called K < N times.

    For Persistent, it limits the amount of threads, distinct from the creator of the unlifting function, it can be called in to N. The amount of calls to the unlifting function within a particular threads is unlimited. The unlifting function will create N copies of the environment when called in N threads and K+1 copies when called in K < N threads.

  • Unlimited

    Unlimited use of the unlifting function.

Instances5Eq, Ord, Show, Generic, Rep

Unlifting functions

6 declarations
valueephemeralConcLimitedUnlift
  1. :: (HasCallStack, forall r. Coercible (effEs r) (Env es -> IO r))
  2. => Env es
  3. -> Int

    Number of permitted uses of the unlift function.

  4. -> ((forall r. effEs r -> IO r) -> IO a)
  5. -> IO a
#

Concurrent unlift with limited uses that doesn't preserve the environment between calls to the unlifting function in threads other than its creator.

valuepersistentConcUnlift
  1. :: (HasCallStack, forall r. Coercible (effEs r) (Env es -> IO r))
  2. => Env es
  3. -> Bool
  4. -> Int

    Number of threads that are allowed to use the unlift function.

  5. -> ((forall r. effEs r -> IO r) -> IO a)
  6. -> IO a
#

Concurrent unlift that preserves the environment between calls to the unlifting function within a particular thread.