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
Mutable constructor fields #8
base: master
Are you sure you want to change the base?
Conversation
Do the constructors need to be magic like that? I rather have a magic way to construct a Ref# so all exposed variants are non-magical. |
Perhaps we constrain the RuntimeReps in -> so whatever magic constructs Ref# is always inlinable / not exists on its own post compilation? |
|
||
The GC write barrier for a mutable constructor may be a little less | ||
efficient than the write barrier for a ``MutVar#``, but this is more | ||
than compensated for by losing a layer of indirection. |
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.
Has the effect of "losing a layer of indirection" been measured and benchmarked? Instinctively, this seems correct to me, that removing a layer of indirection is more beneficial than the performance hit of the GC write barrier for a mutable constructor.
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.
Other people have experimented with hacks to eliminate this layer of indirection (e.g. @ekmett's Struct and @fryguybob's TStruct) and found it worthwhile. Remember that you only pay the performance hit of the write barrier if you ask for it, and it's only likely to be a tiny bit more expensive than the MutVar#
write barrier (which you would be paying anyway), if at all.
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.
The performance benefits from my TStruct can be seen on slide 26 here:
http://www.cs.rochester.edu/u/ryates/files/ryates-IFL2016-slides.pdf
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.
@simonmar can this bit be extended to say why the GC barrier would be less efficient, since it's not obvious to me (so probably not obvious to lots of people).
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.
@dcoutts I think it is the granularity of the dirty header that is different. A mutable pair made of three heap objects will mark exactly which pointer is dirty while a single heap object mutable pair traverses both pointers even if only one changed.
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.
Two reasons: one is the lack of accuracy that @fryguybob mentioned, and the other is more mundane: we know statically the name of stg_MUT_VAR_CLEAN_info
and its DIRTY equivalent, but for an arbitrary mutable constructor we will have to fetch these from the info table.
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.
@simonmar: do you think a layout where we store the number of mutable pointers in the payload might work? Then we could have just two new closure types: MUT_CTR_CLEAN and MUT_CTR_DIRTY, and the layout of the payload would be: [ number of mutable pointers | mutable pointers | non-mutable pointers | non-pointers ]. If we allowed ourselves an extra word in the payload, we may even have some bits for a mask to specify exactly which fields are actually dirty?
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.
Based on my limited understanding, would it be possible to lay out DIRTY and CLEAN info tables in a way that the offset between them is statically known? e.g. MutPair_CLEAN_info+0x420 == MutPair_DIRTY_info
.
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.
I successfully implemented that by having a field in each info table that pointed to the other
info table. They do, as far as I can tell, end up statically offset from each other but it was simple enough to add a field.
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.
That's very interesting! Ideally, I'd hope we can get rid of that additional field, by allocating CLEAN and DIRTY tables next to each other, offset by what I hope is a (info table agnostic) constant. Quite similar to TABLES_NEXT_TO_CODE.
Edward K has a package , Structs, that effectively allows unboxed On Tuesday, September 20, 2016, ramonkeypr notifications@github.com wrote:
|
Cc @ekmett , this seems very related to your structs package / codes. |
formatting tweak
From a surface syntax standpoint, I don't like the magical use of
Differentiating between an |
Can I declare a mutable field with record syntax? If so, what are the typing rules for record selection and update? |
Would it be feasible to support mutable unboxed fields? It would be nice to have an " |
I very much like the fact that this involves the compiler properly knowing about the mutable fields and generating info tables, since this means profiling should work, whereas with @ekmett's cunning hack all your mutable types are the same as far as the profiler can tell. Will there also be a |
Something I've been working on in my own (unpublished, broken, incomplete & in eternal development hell) unboxed-records-with-first-class-labels-library is the freezing & thawing of records, analogous to the way it works for e.g. (im)mutable vectors. That way my main simulation core can run blazingly fast in ST while calculating statistics after a round can be done in a nicely pure interface. Do you see (the value of) any way of extending the current proposal later to support something similar? |
Will it be possible to compare such constructors for reference equality? How would that look? |
I have a bunch of additional features that I want and don't need to be fully worked out yet, but might influence decisions about syntax now. One consideration would be including memory model information. In the GADT form this could be something like
or
or even better:
|
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.
Quite a neat idea, but I have some questions.
proposals/0000-mutable-fields.rst
Outdated
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
|
||
UNPACK must be a no-op on constructors with mutable fields. There's | ||
no sensible way to make UNPACK work with mutable fields. |
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.
Is this to say that we are unable to unpack even immutable fields within a constructor also containing mutable fields? It's not immediately obvious to me why this should be the case. It would be nice to clarify here.
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.
I believe this refers only to unpacking a mutable constructor into another mutable constructor. But it would be nice to unpack the reasoning there. I think it goes like this:
"While C++ or Rust allow unpacking a mutable product type into another, this requires a recursive notion of object construction/initialization and would not fit here. If we tried the following:
data MutPair2 = MP Int {-# UNPACK #-} MutPair
what is the type of the MP
constructor? It cannot be this:
MP :: Int -> MutPair -> IO MutPair2
because the only way to make that work would be to copy the mutable record MutPair
into the MP
constructor. This is (1) not what we'd want for including inner objects as value types, and (2) ruins the guarantee that UNPACK
is a performance hint rather than semantically important."
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.
@rrnewton exactly; I've expanded that section with your text.
proposals/0000-mutable-fields.rst
Outdated
|
||
The ``MutPair`` constructor has the following type:: | ||
|
||
MutPair :: a -> b -> IO (MutPair a b) |
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.
It seems very odd to me to change the type of the constructor like this; this feels like a very ugly exception where the moment you add a mutable field the behavior of your constructor changes in a very fundamental way. It almost feels as though there should be some syntactic indicator to mark these special constructors (e.g. data MutPair = mutable MutPair (IOField a) (IOField b)
, which could also be used in the GADT case).
I tried playing around with a few alternative approaches to provide the "constructor action" via a typeclass (such that we could just hide the constructor itself), but it sadly seems to get quite complicated. My initial thought was that you could identify a constructor with its promoted kind (e.g. 'MutPair
), but the presence of the field arguments in its kind make this rather awkward. To make this fly we'd need to have a better way to represent constructor identity at the type level.
Another approach would be to build this on the existing Generics
infrastructure, although this also seemed quite complicated and not necessarily safe.
Regardless, while changing the type of the constructor itself is awkward, it may be the least of the evils here.
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.
CC @RyanGlScott in case he has thoughts about Generics in this context.
What are the rules for what monad goes on the RHS of a constructor signature? It seems like there is a spectrum here between (1) only supporting raw |
@adamgundry Record selection works smoothly I think - the record selector returns a |
@rwbarton It would be nice to allow mutable unboxed fields. In fact, there's no reason why |
@goldfire yes GADT syntax gives us a great way to specify whether you want |
- Add a section on records - Add a section on mutable unboxed fields - Add various unresolved questions - Move the unresolved GC section to a TODO
I would hope that it's not hard coded to just ST and IO, at the very least I've my own third sibling , Would this boil down to "is the monad thing a new type around the raw internal rep that we use for IO Or ST"? Granted there's then several parallel yaks around fields that form valid mvars or stm variables I guess ? |
@fryguybob why do you say we would need a different |
@simonmar right, this choice would go along with having a memory model. C++11/14/17 whatever distinguishes atomic locations and concurrent accesses (with at least one a write) outside of those locations are undefined behavior. Other approaches may work but at a minimum we should be ensuring that enough information gets down to say the LLVM level, if it is doing the code generation. For instance we may know that an atomic release store on TSO is a normal store, but we don't want LLVM to reorder that store in a basic block. I imagine we are doing the right thing now for this particular case, but it may be very convenient to distinguish explicitly synchronization locations when it does come to having a memory model. In addition, I think volatile is considered orthogonality in LLVM preventing things like accumulating in a register from a value that may be accessed in the same thread from a signal handler. |
Just leaving my reddit comment here for consideration... Yes, I'd also prefer some syntactic clue, that something different is going on. data MutPair a b in IO = MutPair (IOField a) (IOField b) sounds like a possibility. |
@fryguybob I was writing a message earlier to thank you for such detailed answer but it seems I've never sent it, so thank you! Btw, did anything change regarding this topic? I know it's not the highest priority but I'm waiting for it like crazy now! :) |
@wdanilo I don't have any major updates at this point. I will probably have discussions with people about working toward an implementation that can be merged in at some point at Haskell Symposium. |
If the linear types proposal is accepted, we could perhaps someday purely do this at the library level. I'd be interested to see that design fleshed out. |
I think it's worthwhile to describe the difference between a primitive mutable field and a lifted one in full detail because a closure with only primitive mutable fields do not need write barrier, while lifted fields require them. I even prefer the extension can be split into two variations: |
@winterland1989 I would expect the compiler to just do "the right thing" when emitting code (e.g., if it does not need a write barrier it should not emit one). What would be the benefit of having two separate extensions? |
@yav The benefit of a separate
Of course, if we can state all these differences clearly and the compiler is smart enough to always do the right thing, a unified extension is OK. |
@winterland1989 I think we should be fine with just one extension. In my implementation you can specify an unlifted type directly and it does what you would expect. Matching on an unlifted field gives a I think some sort of levity polymorphism would be needed to handle what is normally done with strictness and unpacking. If you had something like:
When you match on an |
@fryguybob The unpacking example probably shouldn't work since we don't allow unpack polymorphic field anyway. But we probably shouldn't allow unpacking arbitrary concrete type either. Do you think it helps if we introduce another |
6b33e58
to
e7fdbc7
Compare
Is someone still working on this? |
@treeowl I've wondered the same thing, I think this would open up lots of interesting Haskell performance which didn't rely on strange aspects of |
I have worked on it some in the last year when I had a little time. https://github.com/fryguybob/ghc/tree/wip/ryates/mutable-fields-9 It is sort of in a state of minimal working example for only |
Now that we have module Data.Mutable.Linear where
type Mutable a = ??? -- Unsure what this should be
readRef :: Mutable a %1-> (Ur a, Mutable a)
writeRef :: a -> Mutable a %1-> Mutable a I'd imagine if we wanted to leverage LTs for this proposal, more changes would also be needed. |
I would be very happy to see progress here! Thanks for your efforts, @fryguybob. I'm not convinced considering linear types adds any value to this proposal. Sure, the API will be unsafe, but then you can always expose a safe, linearly-typed layer over it, as is the case for mutable arrays, for example. The primitives are ST/IO-based (e.g. state-token passing based), and it works well enough. Unless I see a use case that really needs specific treatment for linear types, I don't see why we should complicate the proposal even more. |
@sgraf812 If I understand this proposal correctly, that route would not work very well. Correct me if I'm wrong: The proposed extension generates More concretely: I don't think you can safely write this shim: module Data.Mutable.Linear where
type Mutable a = Data.Mutable.IO.Mutable a
readRef :: Mutable a %1-> (Ur a, Mutable a)
writeRef :: a -> Mutable a %1-> Mutable a because that would allow you to break referential transparency. There's no guarantee that that So I think that would be the value of including a LT option to this extension. We would need a different Whether or not that should go in this extension or a future incremental one 🤷♂️ This proposal has plenty of value as-is and great work already towards implementation thanks to @fryguybob So a future incremental one may make more sense, especially since |
Basically, yes. I'm not familiar with the implementation, but I'm pretty sure that internally we won't have "implementations in IO or ST", but just one that uses state-token passing, e.g. "Make it compatible with Linear Types" isn't a demand that makes sense; You can always use an unsafe non-linear arrow and coerce it into a linear one, offering a safe API over that. You restrict the programs the user can write, thereby making an API that previously allowed unsafe operations into one that only allows safe programs by construction. With that in mind...
I think you want new syntactic sugar for linear types and mutable record fields. E.g. if you have data MutPair a b where
MutPair :: { mutFst :: mutable a
, mutSnd :: mutable b
} -> IO (MutPair a b} You want the corresponding safe, linear API to be "autogenerated" (to the extent that I understand -XLinearTypes): newtype LMutPair a b = LMutPair' (MutPair a b) -- constructor is internal!
pattern LMutPair :: a -> b -> LMutPair a b
get_mutFst :: LMutPair a b %1-> (a, LMutPair a b)
get_mutSnd :: LMutPair a b %1-> (b, LMutPair a b)
set_mutFst :: LMutPair a b %1 -> a %1-> LMutPair a b
set_mutSnd :: LMutPair a b %1 -> b %1-> LMutPair a b
set_mutFst (LMutPair' p) a = unsafePerformIO (writeRef (mutFst p) a) `pseq` LMutPair' p The other functions are defined similarly to
Etc. Finally, I don't see why you couldn't just use TH to derive this API from the
I don't think it's different at all. A data type mit mutable fields that is meant to be used linearly should define a corresponding linear API. |
Frankly, I wish this and the linear types extension were co-designed in the past, rather than trying to retrofit one onto the other after the fact. Being able to tie together the type system changes to low level FFI stuff like this would have been wonderful. |
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.
This proposal was probably written before the general proposal skeleton was added. Nevertheless, I think it would benefit from being adpated to fit that skeleton.
- ``Data.Mutable.IO.Mutable a``, if the constructor has an ``IO`` | ||
return type | ||
- ``Data.Mutable.ST.Mutable s a``, if the constructor has an ``ST s`` | ||
return type, or a ``State# s -> (# State# s, t #)`` return type |
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.
This seems very crude. Ideally, we'd have something like PrimMonad
as a builtin thing and infer PrimMonad m => m t
here.
I don't think we'll see that anytime soon, so an alternative would be a sort of "PrimMonad light":
module Data.Mutable where
class PrimMonadLight m where -- feel free to bike-shed
type Mutable m :: Type -> Type -- or `forall r. TYPE r -> Type` to accomodate runtime-rep poly
readRef :: Mutable a -> m a
writeRef :: Mutable a -> a -> m ()
And corresponding instances for IO
and ST
.
- The new ``mutable`` keyword declares a mutable field | ||
- The return type is in ``IO``: a constructor with any ``mutable`` | ||
fields *must* have a return type that has one of the forms ``IO t``, | ||
``ST s t``, or ``State# s -> (# State# s, t #)``, where ``t`` takes |
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.
Similar to my comment on 1.2.2, it would be much nicer to require a a -> b -> (forall m. PrimMonad m => m (MutPair a b))
return type instead of this quite arbitrary list of permitted types. Although that is not really simple and begs the question whether we can rid ourselves of the constraint in all cases...
fields *must* have a return type that has one of the forms ``IO t``, | ||
``ST s t``, or ``State# s -> (# State# s, t #)``, where ``t`` takes | ||
the form of the normal return type for the constructor. In the | ||
latter two cases, the type variable ``s`` must appear in ``t``. |
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.
latter two cases, the type variable ``s`` must appear in ``t``. | |
latter two cases, the type variable ``s`` must not appear in ``t``. |
|
||
A ``Ref#`` represents a mutable field of a constructor. Although ``Ref#`` | ||
appears to be a normal first-class primitive type, its *runtime | ||
representation* will be ``(# Any, Int# #)``, that is, an unboxed pair |
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.
So why invent an entirely new runtime-rep Ref
then? Hopefully that is just a synonym for TupleRep [ BoxedRep Lifted, IntRep ]
. Also shouldn't this proposal allow mutable fields in unlifted data types (the proposal of which has been accepted, #265)? In that case, Ref#
should take an argument l :: Levity
. E.g.
Ref# :: forall (l :: Levity). * -> * -> TYPE (TupleRep [ BoxedRep l, IntRep ])`
But this proposal appears to rely on Any
, which must have lifted rep today, so maybe supporting unlifted data types isn't possible.
I assume the reason to store the offset separately is to be able to insert the write barrier for the object that contains the field. Is that correct? I'd appreciate a forward reference if so.
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.
It is talking about the implementation here, how the reference itself can be represented by pairing a pointer to a heap object with a mutable field, with the offset to the field. My implementation started before levity polymorphism was completed so I actually have types for lifted and unlifted values in the field that the references points too. I couldn't avoid making a Ref#
type as we need to know rather late how to generate code for accesses to references.
|
||
So, ``{-# UNPACK #-} !T`` cannot do anything if ``T`` is a type with | ||
mutable constructors. However, ``UNPACK`` annotations can be used as | ||
normal on immutable fields in the definition of a mutable constructor. |
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.
Does that mean UNPACK
on a field with a mutable constructor is an error? Or is it just ignored?
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.
I would prefer an error.
|
||
`Eq` is supported by using ``reallyUnsafePtrEquality#`` to compare | ||
mutable constructors, but we must ensure that the constructors are | ||
evaluated strictly in the same way as we do for ``dataToTag#``. |
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.
I really don't want that kind of derived Eq
instance. I find it very misleading; derived Eq
instances normally determine structural equality, not referential equality. For example, users might add a mutable field to some pre-existing record and then wonder why the semantics of the derived Eq
instance change completely.
I don't even see a reason why the Eq
instance couldn't just compute structural equality! A field being mutable doesn't mean reading it is impossible. I certainly would advocate for deriving to have the same semantics as today if it doesn't lead to an error.
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.
I don't even see a reason why the
Eq
instance couldn't just compute structural equality! A field being mutable doesn't mean reading it is impossible. I certainly would advocate for deriving to have the same semantics as today if it doesn't lead to an error.
That would be ideal, and is what Rust does already, but it unfortunately violates referential transparency: reading from a mutable field must be done in a monad, and none is available in a call to Eq
. Perhaps deriving Eq
should be an error, with a separate type class for pointer equality?
|
||
- Its constructor has the declared type | ||
- It has identity, and equality is implemented using pointer equality | ||
(see "Deriving" above). |
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.
Well, that's why you want that weird Eq
instance. I don't like it.
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.
There are really no alternative implementations I can think of that do not violate referential transparency. One could make this an error, though.
operation creates a new one that should compare equal only to itself. | ||
Therefore the usual optimisation for nullary constructors whereby we | ||
share a single global copy of the constructor does *not* apply to | ||
mutable constructors, and we must always allocate them in the heap. |
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.
We have Unique
for that use case. It performs better and is less quirky wrt. evaluatedness.
If you want to somehow optimise Eq
instances for pointer equality, I'd much rather support a design like https://arxiv.org/pdf/2003.01685.pdf. E.g., export a primitive
-- | Semantically equivalent to `\a b k -> k`, but GHC is free to return `True`
-- if `a` and `b` are referentially equal and omit evaluating `k`.
GHC.Exts.withPtrEq :: a -> a -> Bool -> Bool
instance Eq (MutPair a b) where
MutPair a1 b1 == MutPair a2 b2 = withPtrEq a1 a2 (a1 == a2) && withPtrEq b1 b2 (b1 == b2)
= ui, otherwise | ||
|
||
Primitives | ||
^^^^^^^^^^ |
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.
Should this still be nested under "Core"?
* Can we add a way to include mutable arrays in a constructor? | ||
* It would be great to allow STM as an option in addition to IO and | ||
ST. The constructor will need to store extra metadata, because | ||
TVar# is more complex than MutVar#. |
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.
Sounds like another argument for PrimMonadLight. I'm having a hard time imagining what the intended semantics would look like for mutable fields in STM though...
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.
STM with mutable fields was the central part of my thesis and why I made an implementation. The difficulty with pushing that to a high level with something like PrimMonad
is we need extra fields in STM
heap objects (at least that is a desirable design choice).
@sgraf812 my concern about |
I don't think we need to expose the methods of Also note that the proposal already exposes the internal As I said above, I don't see how linear types help. They are just in the type system and have no operational nature. You'd need unsafe primops and hide them behind a linear API (unless you want to extend Core with new LT-specific primitive operations...), which I did in the comment above, using primops this proposal provides. |
I still think this proposal is important, but my bandwidth to work on it is very limited so I'd be more than happy if someone else wanted to pick it up and drive it. |
From the perspective of GHC, it would be so good to have some form of this proposal implemented. I believe that we could have significant savings in allocations in GHC by using a transient (e.g. locally mutable and selectively frozen) IntMap data structure backing Unfortunately, a good source level representation of opt-in mutability eludes me. |
Fwiw: we would use this at work if it were implemented! We need it because we have a system in which we have many small components which are being mutated at high rates. There is an overhead to It's unclear to me whether Simon's original proposal or a linear-types-focused version would have better usability for our use case. The nice thing for us about a linear-types version is that it would the mutation would be able to live outside of |
possible to eliminate a layer of indirection in mutable data | ||
structures in a type-safe way, where currently this can only be done | ||
by using ``unsafeCoerce`` tricks; see for example `the structs | ||
package <http://hackage.haskell.org/package/structs>`_. |
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.
package <http://hackage.haskell.org/package/structs>`_. | |
package <https://hackage.haskell.org/package/structs>`_. |
As a special case, this proposal also provides for the declaration of | ||
constructors with *identity*. That is, constructors that are created | ||
by an explicitly effectful operation, and that can be compared using | ||
pointer-equality. Currently only a few built-in types like ``IORef`` |
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.
Does the pointer equality need to be a primitive, or can it be implemented with a magic type class and reallyUnsafePtrEquality#
?
- mutable fields *must not* have a strictness annotation. (we | ||
anticipate that support for strictness annotations on mutable fields | ||
will be a future proposal). |
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.
I recommend having the default be strict, as that is more likely to be what people who care about performance actually want.
|
||
So, ``{-# UNPACK #-} !T`` cannot do anything if ``T`` is a type with | ||
mutable constructors. However, ``UNPACK`` annotations can be used as | ||
normal on immutable fields in the definition of a mutable constructor. |
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.
I would prefer an error.
|
||
`Eq` is supported by using ``reallyUnsafePtrEquality#`` to compare | ||
mutable constructors, but we must ensure that the constructors are | ||
evaluated strictly in the same way as we do for ``dataToTag#``. |
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.
I don't even see a reason why the
Eq
instance couldn't just compute structural equality! A field being mutable doesn't mean reading it is impossible. I certainly would advocate for deriving to have the same semantics as today if it doesn't lead to an error.
That would be ideal, and is what Rust does already, but it unfortunately violates referential transparency: reading from a mutable field must be done in a monad, and none is available in a call to Eq
. Perhaps deriving Eq
should be an error, with a separate type class for pointer equality?
because the only way to make that work would be to copy the mutable | ||
record ``MutPair`` into the ``MP`` constructor. This is (1) not what | ||
we'd want for including inner objects as value types, and (2) ruins | ||
the guarantee that ``UNPACK`` is a performance hint rather than | ||
semantically important. |
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.
From an imperative language background, it seems that this is what one would want, at least assuming that GHC can avoid the allocation in most cases. UNPACK
might be the wrong pragma, though.
|
||
- Its constructor has the declared type | ||
- It has identity, and equality is implemented using pointer equality | ||
(see "Deriving" above). |
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.
There are really no alternative implementations I can think of that do not violate referential transparency. One could make this an error, though.
This is a proposal to add mutable constructor fields to GHC.
Rendered