diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
 # Revsion history of io-classes
 
+## 1.4.1.0
+
+### Non-breaking changes
+
+* New dependency on `primitive` package
+* New `stToIO` in `MonadST`, which is simpler to use than the existing
+  `withLiftST`, and depends on the `primitive` package's `PrimState` (#141).
+
 ## 1.4.0.0
 
 ### Breaking changes
diff --git a/io-classes.cabal b/io-classes.cabal
--- a/io-classes.cabal
+++ b/io-classes.cabal
@@ -1,6 +1,6 @@
 cabal-version:       3.0
 name:                io-classes
-version:             1.4.0.0
+version:             1.4.1.0
 synopsis:            Type classes for concurrency with STM, ST and timing
 description:
   IO Monad class hierarchy compatible with
@@ -98,6 +98,7 @@
                        async >=2.1,
                        bytestring,
                        mtl   >=2.2 && <2.4,
+                       primitive >= 0.7 && <0.10,
                        stm   >=2.5 && <2.6,
                        time  >=1.9.1 && <1.13
 
diff --git a/src/Control/Monad/Class/MonadST.hs b/src/Control/Monad/Class/MonadST.hs
--- a/src/Control/Monad/Class/MonadST.hs
+++ b/src/Control/Monad/Class/MonadST.hs
@@ -1,35 +1,60 @@
-{-# LANGUAGE RankNTypes #-}
+{-# LANGUAGE InstanceSigs        #-}
+{-# LANGUAGE RankNTypes          #-}
+{-# LANGUAGE ScopedTypeVariables #-}
 module Control.Monad.Class.MonadST (MonadST (..)) where
 
 import Control.Monad.Reader
 
-import Control.Monad.ST (ST, stToIO)
+import Control.Monad.Primitive
+import Control.Monad.ST as ST (ST, stToIO)
 
 
 -- | This class is for abstracting over 'stToIO' which allows running 'ST'
 -- actions in 'IO'. In this case it is to allow running 'ST' actions within
 -- another monad @m@.
 --
--- The type of 'stToIO' is:
+-- The normal type of 'stToIO' is:
 --
--- > stToIO : ST RealWorld a -> IO a
+-- > stToIO :: ST RealWorld a -> IO a
 --
--- Abstracting over this is tricky because we need to not care about both
--- the @IO@, and also the @RealWorld@.
+-- We have two approaches to abstracting over this, a new and an older
+-- (deprecated) method. The new method borrows the @primitive@ package's
+-- 'PrimMonad' and 'PrimState' type family. This gives us:
 --
--- A solution is to write an action that is given the @liftST@ as an argument
+-- > stToIO :: ST (PrimState m) a -> m a
+--
+-- Which for 'IO' is exactly the same as above. For 'ST' it is identity, while
+-- for @IOSim@ it is
+--
+-- > stToIO :: ST s a -> IOSim s a
+--
+-- The older (deprecated) method is tricky because we need to not care about
+-- both the @IO@, and also the @RealWorld@, and it does so avoiding mentioning
+-- any @s@ type (which is what the 'PrimState' type family gives access to).
+-- The solution is to write an action that is given the @liftST@ as an argument
 -- and where that action itself is polymorphic in the @s@ parameter. This
 -- allows us to instantiate it with @RealWorld@ in the @IO@ case, and the local
 -- @s@ in a case where we are embedding into another @ST@ action.
 --
 class Monad m => MonadST m where
+  -- | @since 1.4.1.0
+  stToIO :: ST (PrimState m) a -> m a
+
+  -- | Deprecated. Use 'stToIO' instead.
   withLiftST :: (forall s. (forall a. ST s a -> m a) -> b) -> b
 
+{-# DEPRECATED withLiftST "Use the simpler 'stToIO' instead." #-}
+
 instance MonadST IO where
-  withLiftST = \f -> f stToIO
+  stToIO = stToPrim
+  withLiftST = \f -> f ST.stToIO
 
 instance MonadST (ST s) where
+  stToIO = stToPrim
   withLiftST = \f -> f id
 
-instance MonadST m => MonadST (ReaderT r m) where
+instance (MonadST m, PrimMonad m) => MonadST (ReaderT r m) where
+  stToIO :: ST (PrimState m) a -> ReaderT r m a
+  stToIO f = lift (stToPrim f)
+
   withLiftST f = withLiftST $ \g -> f (lift . g)
