Skip to content

Commit 26dcbbd

Browse files
author
Jaro Reinders
committed
Make Indexing return ordering tokens
1 parent 1a4613d commit 26dcbbd

File tree

3 files changed

+67
-18
lines changed

3 files changed

+67
-18
lines changed

README.md

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,33 @@
22

33
Situation:
44

5-
* All references to an immutable data structure have the same value
6-
* Using immutable data structures makes it easier to reason about your programs
7-
* Mutation can always be avoided by creating a modified copy instead.
8-
9-
Complication:
5+
* There is one essential idea that I must explain properly. Usually in Haskell
6+
you think of everything as a value. However, under the hood most things are
7+
implemented using references. Everytime I say "old version" or "old
8+
reference", I refer to this implementation detail. There is no way to really
9+
explain it without explaining how references are used in the implementation of
10+
Haskell.
1011

11-
* Unfortunately, such copying has a high performance cost.
12-
* In particular, if you only modify a small part of a large data structure, then the whole structure must be copied
13-
* This can turn constant-time operations into linear-time operations.
12+
* Using immutable data structures makes it easier to reason about your programs
13+
(motivation)
14+
* All references to an immutable data structure have the same value
15+
(restriction)
16+
* Mutation can always be avoided by creating a modified copy instead (bad
17+
previous solution).
18+
* Copying can be cheap, if the data that needs to be copied is not much larger
19+
than the data that has been changed
20+
* That happens, for example, when changing the first element of a linked list.
21+
The remainder of the list can be shared in memory.
22+
23+
Complication (why is it bad?):
24+
25+
* Arrays, on the other hand, are an example of a data structure where changes
26+
can be much smaller than the copied data.
27+
* In an array, changing a single element would necessitate a copy of the entire
28+
array.
29+
* Thus, such copying unfortunately has a high performance cost.
30+
* Copying can turn constant-time operations into linear-time operations (and
31+
linear-time into quadratic-time, etc.).
1432

1533
Question:
1634

@@ -19,4 +37,6 @@ Question:
1937
Answer:
2038

2139
* Do the mutation, but keep track of changes
22-
* Whenever the old version is used after the mutation, reapply the changes
40+
* Old references to the same data will be mutated in an equal but opposite way
41+
* Whenever the old version is used after the mutation, reapply the changes
42+

examples/Example/Fleet/Quicksort.hs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,14 @@
22
module Example.Fleet.Quicksort (quicksort) where
33

44
import Fleet.Array
5-
import Data.Tuple (Solo (MkSolo))
65

76
{-# INLINEABLE quicksort #-}
87
quicksort :: Ord a => Int -> Int -> Array a -> Array a
98
quicksort !l !r !xs
109
| r - l <= 1 = xs
1110
| otherwise =
12-
let x@(MkSolo x') = index (r - 1) xs in
13-
x `pseq` case partition l (r - 1) xs x' of
11+
let (x', t) = index (r - 1) xs in
12+
case partition l (r - 1) (tag t xs) x' of
1413
(xs, m) -> quicksort l m (quicksort (m + 1) r (swap (r - 1) m xs))
1514

1615
{-# INLINEABLE partition #-}

src/Fleet/Array.hs

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ module Fleet.Array
2828
, toList
2929
, (!)
3030
, index
31+
, tag
3132
, set
3233
, copy
3334
, swap
@@ -36,7 +37,6 @@ module Fleet.Array
3637

3738
import Prelude hiding (replicate)
3839

39-
import Data.Tuple (Solo (MkSolo))
4040
import GHC.Exts hiding (fromList, toList, Lifted)
4141

4242
import Data.Kind (Type)
@@ -157,25 +157,55 @@ A v0 ! i0 = unsafeDupablePerformIO (go v0 i0) where
157157
| i == j2 -> go v' j1
158158
| otherwise -> go v' i
159159

160+
data Token = Token (State# RealWorld)
161+
162+
returnToken :: a -> IO (a, Token)
163+
returnToken x = IO (\s -> (# s , (x, Token s) #))
164+
160165
-- | Indexing an array. O(1)
161-
-- Using the 'Solo' constructor, you can sequence indexing to happen before
162-
-- future updates without having to evaluate the element itself.
166+
--
167+
-- The tuple and 'Token' serve two purposes:
168+
--
169+
-- - You can now separately force the evaluation of the tuple and the actual
170+
-- array element
171+
-- - You can use the 'Token' to with the 'tag' function on an array to force
172+
-- the indexing to happen before the array can be written to.
163173
{-# INLINE index #-}
164-
index :: Int -> Array a -> Solo a
174+
index :: Int -> Array a -> (a, Token)
165175
index i0 (A v0) = unsafeDupablePerformIO (go v0 i0) where
166176
go v i = do
167177
dat <- readMutVar v
168178
case dat of
169-
Current arr -> MkSolo <$> readMutArray arr i
179+
Current arr -> readMutArray arr i >>= returnToken
170180
-- _ -> error "Accessing old version"
171181
Diff (Set j x) xs
172-
| i == j -> pure (MkSolo x)
182+
| i == j -> returnToken x
173183
| otherwise -> go xs i
174184
Diff (Swap j1 j2) xs
175185
| i == j1 -> go xs j2
176186
| i == j2 -> go xs j1
177187
| otherwise -> go xs i
178188

189+
-- | This is a no-op, but can be used to enforce an ordering between indexing
190+
-- and other array operations, to avoid the overhead of indexing from older
191+
-- versions of the array.
192+
--
193+
-- For example, swapping two elements in an array by using 'index'
194+
-- and 'set' can be done like this:
195+
--
196+
-- @
197+
-- swap :: Int -> Int -> Array a -> Array a
198+
-- swap i j xs =
199+
-- let (x, t1) = index i xs
200+
-- (y, t2) = index j xs
201+
-- in set i y (set j x (tag t1 (tag t2 xs)))
202+
-- @
203+
--
204+
-- This ensures the indexing happens before the setting.
205+
{-# NOINLINE tag #-}
206+
tag :: Token -> Array a -> Array a
207+
tag (Token _) xs = xs
208+
179209
{-# INLINE invert #-}
180210
invert :: MutArray a -> Op a -> IO (Op a)
181211
invert _ (Swap i j) = pure (Swap i j)

0 commit comments

Comments
 (0)