packages feed

data-forced 0.2.0.0 → 0.3.0.0

raw patch · 3 files changed

+170/−215 lines, 3 filesdep ~basePVP ok

version bump matches the API change (PVP)

Dependency ranges changed: base

API changes (from Hackage documentation)

- Data.Forced: [Pairy] :: u -> l -> Pairy u l
- Data.Forced: data Pairy (u :: UnliftedType) (l :: LiftedType) :: UnliftedType
- Data.Forced: data Strict (a :: LiftedType) :: UnliftedType
- Data.Forced: strictlyNF :: forall a. NFData a => a -> StrictValueExtractor (ForcedNF a)
- Data.Forced: strictlyWHNF :: forall a. a -> StrictValueExtractor (ForcedWHNF a)
- Data.Forced: type StrictValueExtractor a = Pairy (Strict a) (Strict a -> a)
+ Data.Forced: (<*>) :: Demand (a -> b) -> Demand a -> Demand b
+ Data.Forced: (>>) :: Demand a -> Demand b -> Demand b
+ Data.Forced: (>>=) :: Demand a -> (a -> Demand b) -> Demand b
+ Data.Forced: data Demand (a :: LiftedType) :: UnliftedType
+ Data.Forced: demandNF :: forall a. NFData a => a -> Demand (ForcedNF a)
+ Data.Forced: demandWHNF :: forall a. a -> Demand (ForcedWHNF a)
+ Data.Forced: extractDemand :: Demand a -> IO a
+ Data.Forced: fmap :: (a -> b) -> Demand a -> Demand b
+ Data.Forced: instance GHC.Show.Show a => GHC.Show.Show (Data.Forced.ForcedNF a)
+ Data.Forced: instance GHC.Show.Show a => GHC.Show.Show (Data.Forced.ForcedWHNF a)
+ Data.Forced: pure :: a -> Demand a
+ Data.Forced: return :: a -> Demand a

Files

data-forced.cabal view
@@ -1,6 +1,6 @@ cabal-version:      3.0 name:               data-forced-version:            0.2.0.0+version:            0.3.0.0 synopsis:           Specify that lifted values were forced to WHNF or NF. license:            MIT license-file:       LICENSE@@ -20,26 +20,26 @@   Main tutorial on the only module. Here is a taste of how it will look   like. -  > import Data.Map.Lazy as ML -- Spine strict-  >-  > -- No references on added leafs even though it is a lazy map.-  > basicEvent :: ML.Map Char (ForcedWHNF Int) -> IO (ML.Map Char (ForcedWHNF Int))-  > basicEvent map0 = do-  >   let-  >     -- Step1: bind the strict value with a strict let. (2 + 2) reduced-  >     -- before val0 is bound.-  >     val0 :: StrictValueExtractor (ForcedWHNF Int)-  >     val0 = strictlyWHNF (2 + 2)-  >     -- val0 = strictlyWHNF (error "argument evaluated") -- would fail-  >-  >     -- Step2: extract the strict value to be use on lazy setting. A-  >     -- neccesary idiom to avoid a pitfall.-  >     val1 = case val0 of { Pairy val0' ext -> ext val0' }-  >-  >     -- Step3: Store the value free of references. Even though map1 is a lazy-  >     -- map, the references to evaluate val1 were already freed.-  >     map1 = ML.insert 'a' val1 map0-  >   pure map1+  > {-# Language QualifiedDo #-}+  > +  > import qualified Data.Forced as DF+  > import Data.Forced hiding (pure, fmap, (\<*\>), return, (>>=), (>>))+  > import Data.Map.Lazy qualified as ML+  > +  > noThunksForWHNF :: IO ()+  > noThunksForWHNF = do+  >   -- map0 actually evaluated on here.+  >   let map0 :: Demand (ML.Map Char (ForcedWHNF Int))+  >       map0 = DF.do+  >         v <- demandWHNF (const (2 + 2) 'a')+  >         DF.pure $ ML.insert 'a' v ML.empty+  > +  >   map1 <- extractDemand map0+  >   go (ML.lookup 'a' map1)+  > +  > -- pattern matching for de-structuring, no construction allowed.+  > go :: ForcedWHNF Int -> IO ()+  > go (ForcedWHNF i) =  print i -- extra-source-files:  source-repository head@@ -47,7 +47,7 @@   location: https://github.com/RubenAstudillo/data-forced  common warnings-    ghc-options: -Wall -Werror=unbanged-strict-patterns+    ghc-options: -Wall  library     import:           warnings
src/Data/Forced.hs view
@@ -3,199 +3,131 @@ {-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE UnliftedDatatypes #-} -module Data.Forced (-    -- * How to use this library--    -- ** Add a new flag on ghc-options-    -- $howToUse1--    -- ** Put ForcedWHNF or ForcedNF types on fields that need to have __no__ references when hold on a long lived data structure.-    -- $howToUse2--    -- ** Use this common idiom whenever you need to obtain a forced value-    -- $howToUse3--    -- * The 'UnliftedType' calling convention (or how to avoid pitfalls)-    -- $unliftedCallingConvetion+module Data.Forced+  ( -- * How to use this library+    -- | You should use the following imports+    --+    -- @+    --   import qualified Data.Forced as DF+    --   import Data.Forced hiding (pure, fmap, (<*>), return, (>>=), (>>))+    -- @ -    -- * Unlifted types+    -- ** Design the type of your long lived data structure+    -- $dataTypeUserDesign -    -- | We need these so whenever we bound a strict computation, all the-    -- lazy values will be forced as needed.-    Pairy (..),-    StrictValueExtractor,-    Strict,+    -- ** Construct values on the @Demand@ monad+    -- $useMonad -    -- * Newtypes that hold a evaluation invariant-    -- $invariantNewtypes-    ForcedWHNF,-    pattern ForcedWHNF,-    ForcedNF,-    pattern ForcedNF,+    -- * Newtypes to be used to specify how evaluated a type should be+    ForcedWHNF (ForcedWHNF)+  , ForcedNF (ForcedNF)+    -- * Monadic environment to execute the needed demands.+  , Demand+  , demandWHNF+  , demandNF+  , extractDemand -    -- * Call By Value functions-    strictlyWHNF,-    strictlyNF,-) where+    -- * Qualified Do support.+    -- | These are available to construct value by hand. But they clash with+    -- 'P.Functor', 'P.Applicative' and 'P.Monad' functions. We cannot+    -- provide instances to those classes as the 'Demand' monad is+    -- 'UnliftedType' kinded. But using @-XQualifiedDo@, GHC will pick up+    -- these names and use it on a @DF.do@ notation that does the right+    -- thing.+  , fmap+  , pure+  , (<*>)+  , return+  , (>>=)+  , (>>)+  ) where  import Control.DeepSeq (NFData (rnf)) import Data.Elevator (LiftedType, UnliftedType)+import Prelude ()+import qualified Prelude as P -{- $howToUse1-Add this to your .cabal file It will save us from a pitfall.+{- $dataTypeUserDesign+The main way this library helps you avoid leaks is by specifying the types+of your long lived data structures. They should contain new demands on the+type variables.  @-common warnings-    ghc-options: -Werror=unbanged-strict-patterns--library-  import: warnings-  ...--executable myAwesomeProgram-  import: warnings-@--}--{- $howToUse2-@ import Data.Map.Lazy -- it is fine, really. import Data.Vector -type MyMap a = Map (ForcedWHNF Char) (ForcedNF (Maybe Vector))+-- On insertion of the lazy map, the keys and the values will evaluated.+type MyMap a = Map (ForcedWHNF Char) (ForcedNF (Maybe (Vector Int))) --- Prompt removal of deleted elements.-type MyMap2 a = ForcedWHNF (Map (ForcedWHNF Char) (ForcedNF (Maybe Vector)))+-- On top, prompt removal of deleted elements.+type MyMap2 a = ForcedWHNF (Map (ForcedWHNF Char) (ForcedNF (Maybe (Vector Int)))) @  This way it will be a type error to store a thunk that is keeping references alive. -} -{- $howToUse3-  1. Strictly @let@ bound on your current context the result of a call to-  'strictlyWHNF' or 'strictlyNF'. __This is the most important part.__-  2. Use a lazy let to extract the underlying @ForcedWHNF a@ or @ForcedNF a@-  with the paired extractor.-  3. Store the previous result on the long lived data structure.+{- $useMonad+We use the 'Demand' monad to construct values with the correct strictness.+You either construct the values by hand, but it is better to use the+@-XQualifiedDo@ extension. +The main functions to keep in mind on this monad are: 'demandWHNF' and+'demandNF'.++Once you have the value specified, you need to extract it to the IO+environment. Hopefully this will be close to main where your long lived data+should be stored. We do this as is the obvious sequence point, so from the+PoV of the rest of the program, the action is visible on the default lifted+environment.+ The ideal code piece looks like this:  @-import Data.Map.Lazy+\{\-\# Language QualifiedDo \#\-\} -type MyMap a = Map Char (ForcedNF (Maybe Int))+import qualified Data.Forced as DF+import Data.Forced hiding (pure, fmap, (\<*\>), return, (>>=), (>>))+import Data.Map.Lazy qualified as ML  noThunksForWHNF :: IO () noThunksForWHNF = do-    let map0 :: ML.Map Char (ForcedWHNF Int)-        map0 = ML.empty--        -- Step 1. Strict let bound is done given the kind of-        -- StrictValueExtractor-        val0 :: StrictValueExtractor (ForcedWHNF Int)-        val0 = strictlyWHNF (const (2 + 2) map0)+    -- map0 actually evaluated on here.+    let map0 :: Demand (ML.Map Char (ForcedWHNF Int))+        map0 = DF.do+          v <- demandWHNF (const (2 + (2 :: Int)) \'a\')+          DF.pure $ ML.insert \'a\' v ML.empty -        -- Step 2. The extractor is inside the Pairy constructor of val0-        val1 = case val0 of { Pairy v ext -> ext v }+    map1 <- extractDemand map0+    go (ML.lookup \'a\' map1) -        -- Step 3. Store as a lazy thunk without the references.-        map1 = ML.insert 'a' val1 map0-    pure ()+-- pattern matching for de-structuring, no construction allowed.+go :: ForcedWHNF Int -> IO ()+go (ForcedWHNF i) =  print i @ -} -{- $unliftedCallingConvetion--Types that have kind 'UnliftedType' have an different calling convention-than normal values. To achieve the correct evaluation level:--  1. We __should__ bound with a name (@let@) computations that return a type-     with 'UnliftedType' kind to the top level of our current context.-  2. We __must not__ inline computation with kind 'UnliftedType' at use-     sites. Specially if the use site is inside of a lazy function.--The first kind of mistake is hard to trigger if we follow the first section-rules. The library also steers you in the right direction by recommending-the following stanza.--@-common warnings-    ghc-options: -Wall -Werror=unbanged-strict-patterns-@--on the cabal file of your project. It will protect your against this common-error--@-noThunksForWHNF :: IO (ML.Map Char (Forced Int))-noThunksForWHNF = do-    let map0 :: ML.Map Char (ForcedWHNF Int)-        map0 = ML.empty--        -- Step 1 & 2 merged-        val0 :: StrictValueExtractor (ForcedWHNF Int)-        val0@(Pairy v ext) = strictlyWHNF (const (2 + 2) map0)--        -- Step 3. Store as a lazy thunk without the references.-        map1 = ML.insert 'a' val1 map0-    pure map1-@--Now @val0@ merged steps 1 and 2. __But in doing so it turned a strict let__-__into a lazy let__. The @-Werror=unbanged-strict-patterns@ will highlight-this at compile time and require you to put a @BangPattern@ on @val0@.--The problem about inlining is hiding a strict computation inside of a lazy-computation. So in the previous example--@-noThunksForWHNF :: IO (ML.Map Char (Forced Int))-noThunksForWHNF = do-    let map0 :: ML.Map Char (ForcedWHNF Int)-        map0 = ML.empty--        -- Step 1 & 2 merged-        val1= case strictlyWHNF (const (2 + 2) map0) of-                Pairy v ext -> ext--        -- Step 3. Store as a lazy thunk without the references.-        map1 = ML.insert 'a' val1 map0-    pure map1-@--val1 __has been bound by a lazy let__. Top level bound plus explicit types-in @let@ bindings will help us to avoid this.--}--{- | Unlifted pair type. When a value of this type is bound, it will have- already evaluated @u@.--}-type Pairy :: UnliftedType -> LiftedType -> UnliftedType-data Pairy (u :: UnliftedType) (l :: LiftedType) :: UnliftedType where-    Pairy :: u -> l -> Pairy u l--{- | A type synonym for the unlifted pair type synonym. It contains a strict- value and a way to extract it to a lazy/normal context.+{- | A strict identity monad of 'UnliftedType' kind. To be used via+@-XQualifiedDo@. -}-type StrictValueExtractor a = Pairy (Strict a) (Strict a -> a)---- | A wrapper for a lifted type that makes sure to have it evaluated.-type Strict :: LiftedType -> UnliftedType-data Strict (a :: LiftedType) :: UnliftedType where-    Strict :: !a -> Strict a+type Demand :: LiftedType -> UnliftedType+data Demand (a :: LiftedType) :: UnliftedType where+    Demand :: a -> Demand a -{- | We don't ship the constructor of 'Strict' as it could be used to bypass- our pushes to bind values to a name.+{- | We don't ship the constructor of 'Demand'. The only way to extract a+'Demand' is to sequence to a know point on 'P.IO'. From the PoV of the rest+of the program, the tagged values with t'ForcedWHNF' or t'ForcedNF'+will have been demanded. -}-extractStrict :: Strict a -> a-extractStrict (Strict a) = a+extractDemand :: Demand a -> P.IO a+extractDemand (Demand a) = P.pure a  {- $invariantNewtypes  The invariants of @ForcedWHNF@ and @ForcedNF@ depends on the constructors not being exported. The only way to construct these value is through the CBV-functions. Pattern matching is done via a unidirectional pattern.+functions of the 'Demand' type. Pattern matching is done via a+unidirectional pattern. -}  {- | Contains a value of type @a@ that has been forced to __W__eak __H__ead@@ -204,6 +136,9 @@ -} newtype ForcedWHNF a = ForcedOuter a +instance P.Show a => P.Show (ForcedWHNF a) where+  show (ForcedOuter a) = "ForcedWHNF " P.++ P.show a+ -- | The only way to extract the underlying value. pattern ForcedWHNF :: forall a. a -> ForcedWHNF a pattern ForcedWHNF a <- ForcedOuter a@@ -213,6 +148,9 @@ -} newtype ForcedNF a = ForcedFull a +instance P.Show a => P.Show (ForcedNF a) where+  show (ForcedFull a) = "ForcedNF " P.++ P.show a+ -- | The only way to extract the underlying value. pattern ForcedNF :: forall a. a -> ForcedNF a pattern ForcedNF a <- ForcedFull a@@ -220,9 +158,41 @@ {- | This is a CBV function. Evaluates the argument to WHNF before returning. -}-strictlyWHNF :: forall a. a -> StrictValueExtractor (ForcedWHNF a)-strictlyWHNF a = Pairy (Strict (ForcedOuter a)) extractStrict+demandWHNF :: forall a. a -> Demand (ForcedWHNF a)+demandWHNF a = a `P.seq` Demand (ForcedOuter a)  -- | This is a CBV function. Evaluates the argument to NF before returning.-strictlyNF :: forall a. NFData a => a -> StrictValueExtractor (ForcedNF a)-strictlyNF a = Pairy (Strict (ForcedFull (rnf a `seq` a))) extractStrict+demandNF :: forall a. NFData a => a -> Demand (ForcedNF a)+demandNF a = rnf a `P.seq` Demand (ForcedFull a)++{- $qualifiedDoSupport++There are no 'P.Functor', 'P.Applicative' or 'P.Monad' classes for+'UnliftedType' types yet. This package is not the right place to define+them. We can get @do@ notation using @-XQualifiedDo@.+-}+-- | 'P.fmap' analogue for 'Demand's which are of the 'UnliftedType' kind.+fmap :: (a -> b) -> Demand a -> Demand b+fmap f (Demand a) = Demand (f a)++-- | Places __no__ demand on the value. 'P.pure' analogue for 'Demand's+-- which are of the 'UnliftedType' kind.+pure :: a -> Demand a+pure a = Demand a++-- | 'P.<*>' analogue for 'Demand's which are of the 'UnliftedType' kind.+(<*>) :: Demand (a -> b) -> Demand a -> Demand b+(<*>) (Demand fun) (Demand fa) = Demand (fun fa)++-- | 'P.return' analogue for 'Demand's which are of the 'UnliftedType' kind.+-- Same as 'pure'.+return :: a -> Demand a+return = pure++-- | 'P.>>=' analogue for 'Demand's which are of the 'UnliftedType' kind.+(>>=) :: Demand a -> (a -> Demand b) -> Demand b+(Demand ma) >>= f = f ma++-- | 'P.>>' analogue for 'Demand's which are of the 'UnliftedType' kind.+(>>) :: Demand a -> Demand b -> Demand b+(>>) fa fb = fa >>= (\_ -> fb)
test/Main.hs view
@@ -1,9 +1,13 @@-{-# OPTIONS_GHC -Wno-unused-binds #-}+{-# Language QualifiedDo #-}+{-# OPTIONS_GHC -Wno-unused-matches #-}+{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-}+{-# HLINT ignore "Evaluate" #-}  module Main (main) where  import Control.Exception (ErrorCall (..), catch)-import Data.Forced+import qualified Data.Forced as DF+import Data.Forced hiding (pure, fmap, (<*>), return, (>>=), (>>)) import Data.Map.Lazy qualified as ML import Test.HUnit (Counts, Test (TestList), runTestTT, (~:)) @@ -24,47 +28,28 @@  noThunksForWHNF :: IO () noThunksForWHNF = do-    let map0 :: ML.Map Char (ForcedWHNF Int)-        map0 = ML.empty-        val0 :: StrictValueExtractor (ForcedWHNF Int)-        !val0@(Pairy val1 ext) = strictlyWHNF (error "argument evaluated")-        map1 = ML.insert 'a' (ext val1) map0-    pure ()--noThunksForWHNF1 :: IO ()-noThunksForWHNF1 = do-    let map0 :: ML.Map Char (ForcedWHNF Int)-        map0 = ML.empty-        val0 :: StrictValueExtractor (ForcedWHNF Int)-        val0 = strictlyWHNF (error "argument evaluated")-        val1 = case val0 of Pairy v ext -> ext v-        map1 = ML.insert 'a' val1 map0-    pure ()--noThunksForWHNF2 :: IO ()-noThunksForWHNF2 = do-    let map0 :: ML.Map Char (ForcedWHNF Int)-        map0 = ML.empty-        val1 = case strictlyWHNF (error "argument evaluated") of Pairy v ext -> ext v-        map1 = ML.insert 'a' val1 map0+    let map0 :: Demand (ML.Map Char (ForcedWHNF Int))+        map0 = DF.do+          v <- demandWHNF (error "argument evaluated")+          DF.pure $ ML.insert 'a' v ML.empty+    map1 <- extractDemand map0     pure () +-- | Should not fail. thunksForWHNFMaybe :: IO () thunksForWHNFMaybe = do-    let map0 :: ML.Map Char (ForcedWHNF (Maybe Int))-        map0 = ML.empty-        val0 :: StrictValueExtractor (ForcedWHNF (Maybe Int))-        val0 = strictlyWHNF (const (Just (error "argument evaluated")) 'a')-        val1 = case val0 of Pairy v ext -> ext v-        map1 = ML.insert 'a' val1 map0+    let map0 :: Demand (ML.Map Char (ForcedWHNF (Maybe Int)))+        map0 = DF.do+          v <- demandWHNF (const (Just (error "argument evaluated")) 'a')+          DF.pure $ ML.insert 'a' v ML.empty+    map1 <- extractDemand map0     pure ()  noThunksForNFMaybe :: IO () noThunksForNFMaybe = do-    let map0 :: ML.Map Char (ForcedNF (Maybe Int))-        map0 = ML.empty-        val0 :: StrictValueExtractor (ForcedNF (Maybe Int))-        val0 = strictlyNF ((+ 1) <$> Just (error "argument evaluated" :: Int))-        val = case val0 of Pairy st ext -> ext st-        map1 = ML.insert 'a' val map0+    let map0 :: Demand (ML.Map Char (ForcedNF (Maybe Int)))+        map0 = DF.do+          v <- demandNF (const (Just (error "argument evaluated")) 'a')+          DF.pure $ ML.insert 'a' v ML.empty+    map1 <- extractDemand map0     pure ()