Skip to content

Commit ec20229

Browse files
authored
Merge pull request #74 from input-output-hk/jdral/strict-mvar-with-invariant
Strict MVars with invariant checking
2 parents df0ddc9 + 1c84274 commit ec20229

File tree

10 files changed

+362
-12
lines changed

10 files changed

+362
-12
lines changed

.github/workflows/cabal.project.local

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ package io-classes
88

99
package strict-mvar
1010
ghc-options: -Werror
11-
flags: +asserts
1211

1312
package strict-stm
1413
ghc-options: -Werror

.github/workflows/haskell.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,9 @@ jobs:
127127
- name: si-timers [test]
128128
run: cabal run si-timers:test
129129

130+
- name: strict-mvar [test]
131+
run: cabal run strict-mvar:test
132+
130133
stylish-haskell:
131134
runs-on: ubuntu-22.04
132135

cabal.project

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,5 @@ package io-sim
3131
package io-classes
3232
flags: +asserts
3333

34-
package strict-mvar
35-
flags: +asserts
36-
3734
package strict-stm
3835
flags: +asserts

strict-mvar/CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,20 @@
11
# Revsion history of strict-mvar
22

3+
## next version
4+
5+
### Breaking changes
6+
7+
* Remove the `asserts` package flag.
8+
9+
### Non breaking changes
10+
11+
* Add a `StrictMVar` with invariant checking in
12+
`Control.Concurrent.Class.MonadMVar.Strict.Checked`.
13+
* Make the checked/unchecked `StrictMVar` modules drop-in replacements of one
14+
another by unifying the interfaces. As a result,
15+
`Control.Concurrent.Class.MonadMVar.Strict` now has `newMVarWithInvariant` and
16+
`newEmptyMVarWithInvariant` functions that ignore the invariant argument.
17+
318
## 1.1.0.0
419

520
### Non breaking changes

strict-mvar/README.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,47 @@ The `strict-mvar` package provides a strict interface to mutable variables
55
for `MVar`s implementations from both
66
[base](https://hackage.haskell.org/package/base-4.17.0.0/docs/Control-Concurrent-MVar.html)
77
and [io-sim](https://github.com/input-output-hk/io-sim).
8+
9+
## Checked and unchecked `StrictMVar`s
10+
11+
There are currently two variant implementations of `StrictMVar`s in this package:
12+
* `Control.Concurrent.Class.MonadMVar.Strict`
13+
* `Control.Concurrent.Class.MonadMVar.Strict.Checked`
14+
15+
The _unchecked_ module provides the simplest implementation of a `StrictMVar`: a
16+
light wrapper around lazy MVars that forces values to WHNF before they are put
17+
into the MVar. The _checked_ module does the exact same thing, but it has the
18+
additional feature that the user can provide an invariant that is checked each
19+
time a new value is placed inside the MVar. The two modules are drop-in
20+
replacements for one another: switching from `*.Strict` to `*.Strict.Checked`
21+
will enable invariant checking, while the converse will disable invariant
22+
checking. To facilitate drop-in replacement, both modules share the same
23+
interface, though in case of the `*.Strict` module, everything related to
24+
invariants will be ignored. This will be explicitly mentioned in the Haddock
25+
documentation of said definitions. For example:
26+
27+
```haskell
28+
-- | The given invariant will never be checked. 'newMVarWithInvariant' is a
29+
-- light wrapper around 'newMVar', and is only included here to ensure that the
30+
-- current module and "Control.Concurrent.Class.MonadMVar.Strict.Checked" are
31+
-- drop-in replacements for one another.
32+
newMVarWithInvariant :: MonadMVar m
33+
=> (a -> Maybe String)
34+
-> a
35+
-> m (StrictMVar m a)
36+
```
37+
38+
**Note:** though the two modules are drop-in replacements for one another, the
39+
`StrictMVar` type from `*.Strict` and the `StrictMVar` type from
40+
`*.Strict.Checked` do not share the same internal representation, and so they
41+
are distinct types.
42+
43+
## Guarantees for invariant checking
44+
45+
Although all functions that modify a checked `StrictMVar` will check the
46+
invariant, we do *not* guarantee that the value inside the `StrictMVar` always
47+
satisfies the invariant. Instead, we *do* guarantee that if the `StrictMVar` is
48+
updated with a value that does not satisfy the invariant, an exception is thrown
49+
*after* the new value is written to the `StrictMVar`. The reason for this weaker
50+
guarantee is that leaving an `MVar` empty can lead to very hard to debug
51+
"blocked indefinitely" problems.

strict-mvar/src/Control/Concurrent/Class/MonadMVar/Strict.hs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,22 @@
22
{-# LANGUAGE TypeFamilies #-}
33
{-# LANGUAGE TypeOperators #-}
44

5-
-- | This module corresponds to 'Control.Concurrent.MVar' in "base" package
5+
-- | This module corresponds to "Control.Concurrent.MVar" in the @base@ package.
66
--
7+
-- Use "Control.Concurrent.Class.MonadMVar.Strict.Checked" as a drop-in
8+
-- replacement for the current module in case you want to check invariants on
9+
-- the values inside 'StrictMVar's.
710
module Control.Concurrent.Class.MonadMVar.Strict
811
( -- * StrictMVar
912
StrictMVar
13+
, LazyMVar
1014
, castStrictMVar
1115
, toLazyMVar
1216
, fromLazyMVar
1317
, newEmptyMVar
18+
, newEmptyMVarWithInvariant
1419
, newMVar
20+
, newMVarWithInvariant
1521
, takeMVar
1622
, putMVar
1723
, readMVar
@@ -65,9 +71,28 @@ fromLazyMVar = StrictMVar
6571
newEmptyMVar :: MonadMVar m => m (StrictMVar m a)
6672
newEmptyMVar = fromLazyMVar <$> Lazy.newEmptyMVar
6773

74+
-- | The given invariant will never be checked. 'newEmptyMVarWithInvariant' is a
75+
-- light wrapper around 'newEmptyMVar', and is only included here to ensure that
76+
-- the current module and "Control.Concurrent.Class.MonadMVar.Strict.Checked"
77+
-- are drop-in replacements for one another.
78+
newEmptyMVarWithInvariant :: MonadMVar m
79+
=> (a -> Maybe String)
80+
-> m (StrictMVar m a)
81+
newEmptyMVarWithInvariant _inv = StrictMVar <$> Lazy.newEmptyMVar
82+
6883
newMVar :: MonadMVar m => a -> m (StrictMVar m a)
6984
newMVar !a = fromLazyMVar <$> Lazy.newMVar a
7085

86+
-- | The given invariant will never be checked. 'newMVarWithInvariant' is a
87+
-- light wrapper around 'newMVar', and is only included here to ensure that the
88+
-- current module and "Control.Concurrent.Class.MonadMVar.Strict.Checked" are
89+
-- drop-in replacements for one another.
90+
newMVarWithInvariant :: MonadMVar m
91+
=> (a -> Maybe String)
92+
-> a
93+
-> m (StrictMVar m a)
94+
newMVarWithInvariant _inv !a = StrictMVar <$> Lazy.newMVar a
95+
7196
takeMVar :: MonadMVar m => StrictMVar m a -> m a
7297
takeMVar = Lazy.takeMVar . mvar
7398

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
{-# LANGUAGE BangPatterns #-}
2+
{-# LANGUAGE CPP #-}
3+
{-# LANGUAGE TupleSections #-}
4+
{-# LANGUAGE TypeFamilies #-}
5+
{-# LANGUAGE TypeOperators #-}
6+
7+
-- | This module corresponds to "Control.Concurrent.MVar" in the @base@ package.
8+
--
9+
-- Use "Control.Concurrent.Class.MonadMVar.Strict" as a drop-in replacement for
10+
-- the current module in case you do /not/ want to check invariants on the
11+
-- values inside 'StrictMVar's.
12+
module Control.Concurrent.Class.MonadMVar.Strict.Checked
13+
( -- * StrictMVar
14+
StrictMVar
15+
, LazyMVar
16+
, castStrictMVar
17+
, toLazyMVar
18+
, fromLazyMVar
19+
, newEmptyMVar
20+
, newEmptyMVarWithInvariant
21+
, newMVar
22+
, newMVarWithInvariant
23+
, takeMVar
24+
, putMVar
25+
, readMVar
26+
, swapMVar
27+
, tryTakeMVar
28+
, tryPutMVar
29+
, isEmptyMVar
30+
, withMVar
31+
, withMVarMasked
32+
, modifyMVar_
33+
, modifyMVar
34+
, modifyMVarMasked_
35+
, modifyMVarMasked
36+
, tryReadMVar
37+
-- * Re-exports
38+
, MonadMVar
39+
) where
40+
41+
import Control.Concurrent.Class.MonadMVar (MonadMVar)
42+
import qualified Control.Concurrent.Class.MonadMVar as Lazy
43+
import GHC.Stack (HasCallStack)
44+
45+
--
46+
-- StrictMVar
47+
--
48+
49+
type LazyMVar m = Lazy.MVar m
50+
51+
-- | A strict MVar with invariant checking.
52+
--
53+
-- There is a weaker invariant for a 'StrictMVar' than for a 'StrictTVar' (see
54+
-- the @strict-stm@ package): although all functions that modify the
55+
-- 'StrictMVar' check the invariant, we do /not/ guarantee that the value inside
56+
-- the 'StrictMVar' always satisfies the invariant. Instead, we /do/ guarantee
57+
-- that if the 'StrictMVar' is updated with a value that does not satisfy the
58+
-- invariant, an exception is thrown. The reason for this weaker guarantee is
59+
-- that leaving an 'MVar' empty can lead to very hard to debug "blocked
60+
-- indefinitely" problems.
61+
data StrictMVar m a = StrictMVar {
62+
-- | The invariant that is checked whenever the 'StrictMVar' is updated.
63+
invariant :: !(a -> Maybe String)
64+
, mvar :: !(LazyMVar m a)
65+
}
66+
67+
castStrictMVar :: LazyMVar m ~ LazyMVar n
68+
=> StrictMVar m a -> StrictMVar n a
69+
castStrictMVar v = StrictMVar (invariant v) (mvar v)
70+
71+
-- | Get the underlying @MVar@
72+
--
73+
-- Since we obviously can not guarantee that updates to this 'LazyMVar' will be
74+
-- strict, this should be used with caution.
75+
--
76+
-- Similarly, we can not guarantee that updates to this 'LazyMVar' do not break
77+
-- the original invariant that the 'StrictMVar' held.
78+
toLazyMVar :: StrictMVar m a -> LazyMVar m a
79+
toLazyMVar = mvar
80+
81+
-- | Create a 'StrictMVar' from a 'LazyMVar'
82+
--
83+
-- It is not guaranteed that the 'LazyMVar' contains a value that is in WHNF, so
84+
-- there is no guarantee that the resulting 'StrictMVar' contains a value that
85+
-- is in WHNF. This should be used with caution.
86+
--
87+
-- The resulting 'StrictMVar' has a trivial invariant.
88+
fromLazyMVar :: Lazy.MVar m a -> StrictMVar m a
89+
fromLazyMVar = StrictMVar (const Nothing)
90+
91+
newEmptyMVar :: MonadMVar m => m (StrictMVar m a)
92+
newEmptyMVar = fromLazyMVar <$> Lazy.newEmptyMVar
93+
94+
newEmptyMVarWithInvariant :: MonadMVar m
95+
=> (a -> Maybe String)
96+
-> m (StrictMVar m a)
97+
newEmptyMVarWithInvariant inv = StrictMVar inv <$> Lazy.newEmptyMVar
98+
99+
newMVar :: MonadMVar m => a -> m (StrictMVar m a)
100+
newMVar !a = fromLazyMVar <$> Lazy.newMVar a
101+
102+
newMVarWithInvariant :: MonadMVar m
103+
=> (a -> Maybe String)
104+
-> a
105+
-> m (StrictMVar m a)
106+
newMVarWithInvariant inv !a =
107+
checkInvariant (inv a) $
108+
StrictMVar inv <$> Lazy.newMVar a
109+
110+
takeMVar :: MonadMVar m => StrictMVar m a -> m a
111+
takeMVar = Lazy.takeMVar . mvar
112+
113+
putMVar :: MonadMVar m => StrictMVar m a -> a -> m ()
114+
putMVar v !a = do
115+
Lazy.putMVar (mvar v) a
116+
checkInvariant (invariant v a) $ pure ()
117+
118+
readMVar :: MonadMVar m => StrictMVar m a -> m a
119+
readMVar v = Lazy.readMVar (mvar v)
120+
121+
swapMVar :: MonadMVar m => StrictMVar m a -> a -> m a
122+
swapMVar v !a = do
123+
oldValue <- Lazy.swapMVar (mvar v) a
124+
checkInvariant (invariant v a) $ pure oldValue
125+
126+
tryTakeMVar :: MonadMVar m => StrictMVar m a -> m (Maybe a)
127+
tryTakeMVar v = Lazy.tryTakeMVar (mvar v)
128+
129+
tryPutMVar :: MonadMVar m => StrictMVar m a -> a -> m Bool
130+
tryPutMVar v !a = do
131+
didPut <- Lazy.tryPutMVar (mvar v) a
132+
checkInvariant (invariant v a) $ pure didPut
133+
134+
isEmptyMVar :: MonadMVar m => StrictMVar m a -> m Bool
135+
isEmptyMVar v = Lazy.isEmptyMVar (mvar v)
136+
137+
withMVar :: MonadMVar m => StrictMVar m a -> (a -> m b) -> m b
138+
withMVar v = Lazy.withMVar (mvar v)
139+
140+
withMVarMasked :: MonadMVar m => StrictMVar m a -> (a -> m b) -> m b
141+
withMVarMasked v = Lazy.withMVarMasked (mvar v)
142+
143+
-- | 'modifyMVar_' is defined in terms of 'modifyMVar'.
144+
modifyMVar_ :: MonadMVar m => StrictMVar m a -> (a -> m a) -> m ()
145+
modifyMVar_ v io = modifyMVar v io'
146+
where io' a = (,()) <$> io a
147+
148+
modifyMVar :: MonadMVar m => StrictMVar m a -> (a -> m (a,b)) -> m b
149+
modifyMVar v io = do
150+
(a', b) <- Lazy.modifyMVar (mvar v) io'
151+
checkInvariant (invariant v a') $ pure b
152+
where
153+
io' a = do
154+
(!a', b) <- io a
155+
-- Returning @a'@ along with @b@ allows us to check the invariant /after/
156+
-- filling in the MVar.
157+
pure (a' , (a', b))
158+
159+
-- | 'modifyMVarMasked_' is defined in terms of 'modifyMVarMasked'.
160+
modifyMVarMasked_ :: MonadMVar m => StrictMVar m a -> (a -> m a) -> m ()
161+
modifyMVarMasked_ v io = modifyMVar v io'
162+
where io' a = (,()) <$> io a
163+
164+
modifyMVarMasked :: MonadMVar m => StrictMVar m a -> (a -> m (a,b)) -> m b
165+
modifyMVarMasked v io = do
166+
(a', b) <- Lazy.modifyMVar (mvar v) io'
167+
checkInvariant (invariant v a') $ pure b
168+
where
169+
io' a = do
170+
(!a', b) <- io a
171+
-- Returning @a'@ along with @b@ allows us to check the invariant /after/
172+
-- filling in the MVar.
173+
pure (a', (a', b))
174+
175+
tryReadMVar :: MonadMVar m => StrictMVar m a -> m (Maybe a)
176+
tryReadMVar v = Lazy.tryReadMVar (mvar v)
177+
178+
--
179+
-- Dealing with invariants
180+
--
181+
182+
-- | Check invariant
183+
--
184+
-- @checkInvariant mErr x@ is equal to @x@ if @mErr == Nothing@, and throws an
185+
-- error @err@ if @mErr == Just err@.
186+
checkInvariant :: HasCallStack => Maybe String -> a -> a
187+
checkInvariant Nothing k = k
188+
checkInvariant (Just err) _ = error $ "StrictMVar invariant violation: " ++ err

strict-mvar/strict-mvar.cabal

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,11 @@ source-repository head
2424
location: https://github.com/input-output-hk/io-sim
2525
subdir: strict-mvar
2626

27-
flag asserts
28-
description: Enable assertions
29-
manual: False
30-
default: False
31-
3227
library
3328
hs-source-dirs: src
3429

3530
exposed-modules: Control.Concurrent.Class.MonadMVar.Strict
31+
, Control.Concurrent.Class.MonadMVar.Strict.Checked
3632
default-language: Haskell2010
3733
build-depends: base >= 4.9 && <4.19,
3834
io-classes >= 1.0 && <1.2
@@ -44,5 +40,25 @@ library
4440
-Wpartial-fields
4541
-Widentities
4642

47-
if flag(asserts)
48-
ghc-options: -fno-ignore-asserts
43+
test-suite test
44+
type: exitcode-stdio-1.0
45+
hs-source-dirs: test
46+
main-is: Main.hs
47+
48+
other-modules: Test.Control.Concurrent.Class.MonadMVar.Strict.Checked
49+
default-language: Haskell2010
50+
build-depends: base >=4.9 && <4.19,
51+
io-sim,
52+
QuickCheck,
53+
tasty,
54+
tasty-quickcheck,
55+
strict-mvar,
56+
57+
ghc-options: -Wall
58+
-Wno-unticked-promoted-constructors
59+
-Wcompat
60+
-Wincomplete-uni-patterns
61+
-Wincomplete-record-updates
62+
-Wpartial-fields
63+
-Widentities
64+
-fno-ignore-asserts

strict-mvar/test/Main.hs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
module Main where
2+
3+
import qualified Test.Control.Concurrent.Class.MonadMVar.Strict.Checked as Checked
4+
import Test.Tasty
5+
6+
main :: IO ()
7+
main = defaultMain Checked.tests

0 commit comments

Comments
 (0)