-
Notifications
You must be signed in to change notification settings - Fork 139
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
add Mutable.mapM et al, clarify that Mutable.mapM_ does not modify #417
base: master
Are you sure you want to change the base?
Conversation
I suppose one issue here is non-deterministic monads, and perhaps the reason why this wasn't added in the first place - though I'd argue with non-deterministic monads, the caller would take responsibility for the order in which the modifications are applied, and that this PR is still helpful and safely predictable for deterministic monads (the more common use-case). I could add this caveat to the documentation if you want. In any case, I think the documentation for |
This would fix most of #334. The discussion about naming convention is also relevant, afaik it was the reason these functions weren't added at first. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks a lot for PR. First issue is naming? it was discussed in in #334. General consensus is to add add some suffix to signal that function mutates vector in place. InPlace
/Mod
/Mut
have been proposed. I slightly lean toward InPlace
. @lehins, @Bodigrim what is your opinion?
Also please add definitions for all module with specializations (storable,unboxed,primitive,vector). Same for comments
P.S. Copying comments by hand is chore so wrote program to copy haddocks from generic module https://github.com/Shimuuar/vector-doctweak could be useful
I'm in favor of |
IMO the suffix is unnecessary because the type signature already communicates mutable behaviour - it takes in a |
To give a bit more detail: my perspective is shaped by the fact you can convert immutable and mutable operations between each other generically (e.g. using https://hackage.haskell.org/package/mutable-lens), so the immutable and mutable mapM (traverse) operations are the same thing; the fact that one version modifies in-place whereas another version returns a copy, is a property of the data structure, not the operation. Here we are defining the operation on Vector.Generic.Mutable already, there is no need to add |
In case one argues "it's confusing when something modifies in-place when one expects it not to"; but it's also confusing when something does not modify when one expects it to, which is why I wrote this PR the first place - I had naively expected Therefore IMO it is sufficient to document that |
Fwiw, the reason for that is so that people don't necessarily have to add
Well, it could also return a new vector (which might even be preferrable sometimes, since it can have a different element type, like for the standard |
There are 3 such functions we can have for mutable vectors:
mapM_ :: PrimMonad m => (a -> m b) -> MVector (PrimState m) a -> m ()
mapM :: PrimMonad m => (a -> m b) -> MVector (PrimState m) a -> m (MVector (PrimState m) b)
mapInPlaceM :: PrimMonad m => (a -> m a) -> MVector (PrimState m) a -> m () @infinity0 we need some new suffix in order to disambiguate. None of the common |
I think in the context of Haskell naming conventions having |
I want to re-emphasise my point above that you can convert immutable and mutable operations between each other generically (e.g. using https://hackage.haskell.org/package/mutable-lens), In other words, mapM on an immutable data structure with type signature
This does not make sense to be called You wouldn't call the latter function Instead, I argue this naming: mapM_ :: PrimMonad m => (a -> m b) -> MVector (PrimState m) a -> m ()
mapM :: PrimMonad m => (a -> m b) -> MVector (PrimState m) a -> m ()
cloneMapM :: PrimMonad m => (a -> m b) -> MVector (PrimState m) a -> m (MVector (PrimState m) b) is more consistent with the existing convention on immutable structures. |
The large red flag with a mutate-in-place
|
@infinity0 I want to re-emphasize my point, function that you are suggesting (note that I fixed mapM :: PrimMonad m => (a -> m a) -> MVector (PrimState m) a -> m () will not get into vector because there are already too many people who agree that it is a bad idea, including all Also note, rust and linear haskell have nothing to do with this PR because we are not Rust and linear haskell is faaaaaar away from being mainstream. |
I simply copied and pasted what you wrote in your comment, above mine.
I was just trying to change your minds by noting things that haven't been previously observed during the discussions. That is what discussions are for, for changing people's minds. I brought up rust in response to somebody else that brought up rust. I am not sure how you define "mainstream" if the current version of GHC 9 isn't "mainstream". Comparatively, it's made more progress than a lot of Haskell libraries, in quite an impressive short amount of time actually, so it's much closer than you think. Of course if the answer is a simple "no", my only response is to predict that this discussion will look quite short-sighted when (not if) linear haskell does become standard haskell. |
You did not just copy and paste. This is what I wrote in #417 (comment): mapM :: PrimMonad m => (a -> m b) -> MVector (PrimState m) a -> m (MVector (PrimState m) b) This is what you wrote: mapM :: PrimMonad m => (a -> m b) -> MVector (PrimState m) a -> m () Two very different things.
This is totally fine, we all happy to discuss possibilities. I am definitely not opposed to discussing things, but if there is pushback from everyone else, there is no need to "re-emphasise".
We barely dropped support for ghc-7.10 a month ago in #400 so ghc-9 is far away from minimal ghc version vector supports.
It is much closer to being adopted? Maybe, hopefully. But core packages that everyone depends on cannot switch to using all the new bells and whistles provided by the newest version of ghc, too many people depend on |
Why do you two keep quarreling about just types? Write codes: import Data.Vector.Generic.Mutable as M
import Data.Vector.Generic as G
import Control.Monad.Primitive
M.mapM1, M.mapM2 :: (M.MVector v a, M.MVector v b, PrimMonad m) => (a -> m b) -> v (PrimState m) a -> m (v (PrimState m) b)
-- equivalent to `G.freeze . G.mapM f . G.thaw`
M.mapM1 f veca = do
veca' <- M.clone veca
vecb <- M.new (M.length veca)
forM_ [0..M.length veca - 1] $ \ !i ->
M.write vecb i =<< (f =<< M.read veca' i)
return vecb
-- equivalent to the intention of undefined behavior code `G.unsafeFreeze . G.mapM f . G.unsafeThaw`
M.mapM2 f veca = do
vecb <- M.new (M.length veca)
forM_ [0..M.length veca - 1] $ \ !i ->
M.write vecb i =<< (f =<< M.read veca i)
return vecb
M.mapInPlaceM f :: (M.MVector v a, PrimMonad m) => (a -> m a) -> v (PrimState m) a -> m ()
M.mapInPlaceM f vec = do
forM_ [0..M.length vec - 1] $ \ !i ->
M.write vec i =<< (f =<< M.read vec i) I'm not really interested in isomorphism of types, but they seem to be very different functions. (By the way, if we implement And we need to make sure we document the order of execution of |
I think the suffix is not so bad and will open the way to algorithms having two variants which is nice and good. Probably you can also have fusion rules that allow not more clones than is necessary by having both variants. Overall, if linear haskell becomes the default then I am sure vector would be happy at that time to get with the program. Until then, is the name so bad? |
True, but I just hope all of us will live long enough to witness this glorious future. Shall we put this PR on hold and return to the discussion at that time? @infinity0, it's not very long-sighted for purposes of a constructive discussion to brand maintainers short-sighted. If you are unhappy with our position, you can escalate this question to CLC. |
@gksato Function type should be enough in this case to understand what the function is doing. For example your @Boarders I think fusion rules are our of window for mutable |
Love it 😄
👍 |
@lehins More than fair, if you are mutating then it is easy to figure out fusion by hand. Not that Haskell is another language (as the discussion has established) but I did hear a C++ programmer recently say they wish the |
@lehins, Ohhhh... embarrassing. Firstly, I fixed my code. Secondly, I didn't even realize I didn't get to read through his/her discussion till now. I'm terribly sorry, @infinity0. You did understand the intention of (Turning my head to @lehins again) |
@gksato No worries, your input is always welcomed 👍 |
I want to point out that With mutable data structure we have two different strategies: "create copy" and "update in place". Both are useful, but very different, so names have to make clear which strategy is used by function. Functions for pure data structures always create copy (except when opportunistic optimizations kick in). So it makes perfect sense to name functions that create copies of mutable containers the same way as functions that works with immutable and use different naming convention for functions that update in place Linear types may give us update in place for pure data structures too. But this is new and experimental extension which I think will change as things are ironed out and we understand how to use them in larger haskell ecosystem. And vector is foundational library and by necessity is conservative. |
Yes, I was taking some artistic license with the use of that term. However under linear haskell, In that situation, having a separate naming convention is really unnecessary, directly analogous to naming So the main question is do we want to have these other duplicate names for 2-5 years and encourage people to write code like that, based on a naming convention that is a evolutionary dead-end in terms of technology, and not only that, it increases the pain for writing linear code later? My argument has been no. But sure, if the consensus is "yes" then by all means somebody is very welcome to make the minimal changes to this PR and get it merged; I myself personally can wait a bit longer. |
Um,... that's totally true if we take the viewpoint focused on do v <- generateM 10 (return . id)
v2 <- mapM (return . (fromIntegral :: Int -> Word) . (*2)) v
val <- read v 2
val2 <- read v2 2
assert (val == 2)
assert (val2 * 2 == 4)
-- with this code, we don't need to decide we're using MVector or Vector
VU.read :: Applicative m => VU.Vector a -> Int -> m a
VU.read v i = pure $ v VU.! i I don't think I have prattled on my harsh opinion so far, but as I writing this, I see one more important point now: if it is |
And with Linear Haskell... yes, I think |
Yes my suggestion breaks this, in favour of emphasising the relationship between mutable vs immutable fmap. If you think of MVector as a "handle" (like an STRef) and the Vector as the actual data, it makes sense that Now you could argue that
I think this is more related to the underlying representation rather than the current mutable vs immutable discussion - if |
Unfortunately no, since an applied vector might be a slice. You can't change the constituent type of the first half of a vector while the second half staying the same. However you're making more sense to me now (I now see your view point: |
With linear types this would actually work as you couldn't keep a reference to both the outer slice and the inner slice at the same time. (You could split the outer slice into multiple pieces, then change the type of each piece, that would work.) Re references & "isomorphism" etc, yes I can appreciate how this unification vision of mutable + immutable forms appears clumsy with the current state of things. |
I want to stress that linear API is not going to replace non-linear API, it's going to exists alongside. And this PR expands nonlinear API so concerns about linear API are not terribly relevant to problem at hand |
No description provided.