-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
A mutable reference *is* a lens #2
Comments
Case in point, here is a direction conversion from {-# LANGUAGE MagicHash #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE UnboxedTuples #-}
module Control.Lens.Mutable where
import Control.Lens.Type (Lens')
import GHC.ST (ST(..), STRep(..))
import GHC.STRef (STRef(..))
import GHC.Base (State#, readMutVar#, writeMutVar#)
type STLens s a = Lens' (S s) a
data S s = S (State# s) -- hack to lift State#
stRefToLens :: STRef s a -> STLens s a
stRefToLens (STRef var#) f = \(S s1#) ->
let (# s2#, valr #) = readMutVar# var# s1#
in fmap (\valw -> S (writeMutVar# var# valw s2#)) (f valr)
stateToST :: (S s -> (a, S s)) -> ST s a
stateToST state = ST $ \s1# -> let (a, S s2#) = state (S s1#) in (# s2#, a #)
stateSTRef :: STRef s a -> (a -> (x, a)) -> ST s x
stateSTRef ref state = stateToST $ stRefToLens ref state edit: generalised to work with any functor, like a lens can |
Huh! I guess this is "obvious" if you consider To be honest I originally set out to make this generic over things that aren't |
If I "commit" to a lens into |
Cool! I expanded the above code into a mini-library here, feel free to take ideas or even code from it. For now it just has a few lens for single-value references, it doesn't do your |
thanks for this again :) I've had a chance to look into this deeper and going into "mutable variables are lenses" as a basis for this library. the main issue i ran into was that while this covers |
@mstksg I ran into this as well :) I created a new typeclass called Allocable. For symmetry I added Example instances are here though this is just a toy example in a very experimental repo. I also did some basic benchmarking that I didn't push yet, but I found this to be ~2x slower than using the reference directly. (In a real program the overall performance hit will likely be smaller than 2x, since your program would be doing stuff other than reading/writing to the reference.) Not great, but not terrible. {-# LANGUAGE DataKinds, LambdaCase, TypeApplications #-}
{-
cabal new-build --enable-profiling
cabal new-exec --enable-profiling ghc -- -package mutable-lens -threaded -rtsopts -prof -fprof-auto BasicBenchmark.hs && ./BasicBenchmark +RTS -hr
-}
import Control.Lens
import Control.Lens.Mutable
import Data.Primitive.MutVar
import Control.Concurrent
import GHC.Exts
import System.CPUTime
import System.Mem
whileJustM :: (Monad m, Monoid a) => m (Maybe a) -> m a
whileJustM act = go mempty
where
go accum = act >>= \case
Just r -> go $! accum <> r
Nothing -> pure accum
iterations = 100000000
test1 :: IO ()
test1 = do
r <- newMutVar 0
whileJustM $ do
v <- readMutVar r
--threadDelay 1
writeMutVar r (v + 1)
if v < iterations
then pure $ Just ()
else pure Nothing
test2 :: IO ()
test2 = do
r <- stToM $ alloc @(S 'OpST RealWorld) @Int @(MutVar RealWorld) 0
whileJustM $ do
v <- stToM $ asLens @(S 'OpST RealWorld) @Int @(MutVar RealWorld) r $ \v -> (v, v)
--threadDelay 1
stToM $ asLens @(S 'OpST RealWorld) @Int @(MutVar RealWorld) r $ \v -> ((), v + 1)
if v < iterations
then pure $ Just ()
else pure Nothing
time :: String -> IO a -> IO a
time n a = do
start <- getCPUTime
putStrLn $ n <> ": start"
r <- a
end <- getCPUTime
putStrLn $ n <> ": time " <> show (end - start)
pure r
main :: IO ()
main = do
time "test1" test1
performGC
time "test2" test2
performGC
time "test1" test1
performGC
time "test2" test2 |
This is more of a comment for discussion, not a bug report. I was reading the docs and the comment "A MutPart m s a is spiritually similar to a lens" prompted me to write up my thoughts.
A theme I've realised recently is that a reference very literally is a lens, they are one and the same thing. All stateful monads that can be "referenced" into, have a state type parameter; in
PrimMonad
this isPrimState
, forST s
this iss
and forIO
it isRealWorld
. A reference is just an index into part of this overall state.One advantage is that the user cannot choose
s
, it is kept either forcibly parametric that the user can't instantiate, or a phantom type likeRealWorld
that the user cannot instantiate either. This forces stateful code to be compositional - unlikeStateT s
where a common pattern (and the "obvious intuition") is to instatiates
to something, or place static constraints upon it. Indeed, when dealing withStateT s
I've found it useful to not instantiate/constrains
and rather have the user pass in alens
; this avoids nasty issues such as:StateT
StateT s
action as part of the state itself; attempting to instantiates
will result in lots of "cannot construct infinite type" errorsand using a
lens
avoids all of these issues. The code you end up with looks very similar to mutable code using references - your have to pass references around, you pass lens around.In fact, this is not surprising, the typical way to implement a reference within a stateful monad like
IO
orST
is for the overall state type to be indexable, and for each reference value to carry this index - exactly like aStore
comonad which is what a lens is. For every reference, you will be able to write something like(a -> f b) -> (s -> f t)
- given a way to mutate the reference, it gives a way to mutate the overall state. In fact this is a great way to encode the reference itself, you do not need a separate "reference" vs a "lens" type.Instead of defining
MutVar
,IORef
, all of these separate types, one can simply return a lens that acts overRealWorld
, etc, perhaps wrapped up in a generic newtype for the user. This sort of pattern also obsoletesMutPart
since this can be represented simply by another lens.The text was updated successfully, but these errors were encountered: