Closed bendmorris closed 6 years ago
Here's a basic prototype: https://github.com/bendmorris/haxe/tree/const
What is the advantage of this over the final
keyword (which is already implemented in Haxe 4)?
The use cases are pretty different. final
simply disallows reassignment of a class field. Const
prevents any modification of any value, recursively - so e.g. an Array that you can iterate over but not push to or modify its members. It doesn't prevent reassignment, and isn't limited to fields.
final
isn't (or isn't supposed to be) limited to fields. That's just the obvious first case -- and may be the only thing implemented at the moment. IIRC, when folks were discussing it, it was for any variable (locals and fields) and could be applied to classes and methods as well. (Though the concept of final for classes and methods is a bit murky to me since it isn't about inheritance as it is for e.g. Java.)
I think that simply applying Const
That is, unless I'm misunderstanding the whole concept here.
Is Const needed for Ocaml target?
Can you provide me with a really simple practical and useful use of Const - in relation to Array<Sprite>
Is this a feature like 'Abstract' that I use all the time or something like 'Options' which for me is quite marginal and I would not really care if it was not there tomorrow, seems more like the 'Options' feature?
final
prohibits reassignment; that's an entirely orthogonal concept here.
unless you now add some new metadata to each method that describes the enforcement policy
This is specifically in the proposal. I have a working prototype which I linked above, which adds the metadata to Array. Yes, it specifically only works when the metadata is there; otherwise it only allows field access by default. Languages like Rust also require declaring mutability; it's not a new concept. The main difference here is that everything is mutable by default and immutability is opt in.
And, even if you do add such metadata, it has to become a run-time construct because we cannot guarantee that the object won't be passed around to some other code that will call functions with side effects.
I don't think it has to at all. It's explicitly adding a set of compile time checks, which you have ways to get around. It doesn't have to be exhaustive or apply at runtime to be useful.
Can you provide me with a really simple practical and useful use of Const - in relation to Array
I'll give you one example that triggered this proposal: in HaxePunk each scene maintains a list of entities in the scene which have a specific type (a Map<String, List<Entity>>
). We have a public method to get a list of entities that have a specific type. Currently I have two options:
This proposal adds a third option: return the list as a Const<List<Entity>>
. This returns the same list that's already allocated, but the compiler will prevent the user from modifying the list or its contents, throwing a compiler error if they do so.
It seems to me that your example problem could be solved, at least in part, via composition (a wrapper object). But that's neither here nor there.
final
prohibits reassignment; that's an entirely orthogonal concept here.
Constfinal
cannot be applied to a return value (yet), so I understand your desire here. Even if it did, it would apply to the assignment of the variable, and not to the internals of the object.
One trouble I see is that we then need to add @:const to the StdLib in order to make this useful -- even to you. All of a sudden, it's not optional for the compiler team.
It doesn't have to be exhaustive ... to be useful.
Ah, the bugs that will be generated! If it's not exhaustive, it's a maintenance headache.
Languages like Rust also require declaring mutability; it's not a new concept.
Correct, it was added as a similar (opt-in) concept to C89, almost 30 years ago. And engineers have hated it ever since.
Const prevents any modification of any value, recursively
In your example above, a Const<List
var i : Const<List<SomeType>> = MyLib.getSomeType();
for (type in i) {
var otherStuff = type.getOtherStuff();
var extraStuff = otherStuff.getExtra();
extraStuff.setProperty(newValue); // <<<-------- Should create an error!
}
It seems to me that your example problem could be solved via composition (a wrapper object). But that's neither here nor there.
It could. This is a generic way to construct such wrappers automatically, so that I don't have to create one per type myself. I could also do this with a macro, which I addressed under "alternatives."
Const also prohibits reassignment.
It doesn't - I'm not sure where you're getting this idea. It prohibits mutating the object, not reassigning references:
final x:{x:Int, y:Int} = {x:1, y:2};
// this is invalid; I can't reassign a final
x = {x:3, y:4};
// this is fine; finals aren't recursively immutable, only the reference is immutable
x.y += 2;
var x:Const<{x:Int, y:Int}> = {x:1, y:2};
// this is fine; I can reassign a Const
x = {x:3, y:4};
// this is invalid; I can't mutate a Const
x.y += 2;
These are entirely different features. Const prohibits reassigning fields within the object, and final does not - it's a big difference.
All of a sudden, it's not optional for the compiler team.
And that's why changes like this go through a proposal process. Given that @nadako was thinking along similar lines, I'm hoping the compiler team will see the value added by this change as greater than the cost of maintaining it in the standard library. I think this will be appreciated by people coming from a functional programming background.
In your example above, a Const
is returned -- but you want the Const-ness to apply to SomeType (and its methods and return values) recursively without explicitly declaring it.
I don't know where I'm missing you, but I addressed this exact problem and proposed a solution. Members accessed from the List are automatically Const, so they can't be modified. If Entity also declares immutable methods, I can use them; if it doesn't, I can only access its fields. But obviously I'm also going to mark up Entity's fields, or I wouldn't be using this construct in the first place.
Notably, your example works correctly, right now in my prototype! 😄
@Justinfront in your case it seems like a custom abstract would be a better solution if you want limited mutability.
Ben there is certainly a problem that abstracts can't be inherited, but for the Fixel use case they could do something like this: https://try.haxe.org/#6af57 try haxe does not currently have final so I can't really explore mixing it in within a try example and I am not sure I have final in my working haxe version, there are quite a lot of options and variations that allow a lot of flexibility without java restrict type approach.
So, I've figured out two things: The first is that I am conflating a variable (e.g. a reference) and what it contains (a value), even though they are distinct and I know it. I believe that it comes from the way I think when I am writing code -- and I'm trying to observe this solution from that perspective. Thus, I'm sorry if I have sounded belligerent here. I'm definitely NOT trying to be.
Second, I've read the code, and while I only have a passing understanding of OCaml, I think I see how you are applying const-ness across sub-element access. That can only apply within a single scope (and its contained scopes), though. I think it still breaks down when the sub-entity is passed into another routine -- unless that receiving routine's parameters are marked Const
The intention behind returning a Const value in the first place is to limit what the caller does with it. That's by design.
With Const<T> from T
it should not be named Const
, but ReadOnly
which is a different thing:
typedef HasX = { var x:Int = 0; }
var o:X = { x: 0 }
var c:Const<HasX> = o;
var initial = c.x;//this should not change, because it's recursively *constant*, right?
o.x = 12;
assert(initial == c.x);//oooops
This is not just a nuance: treating read only data structures as immutable ones can fail phenomenally in concurrent code.
//this should not change, because it's recursively constant, right?
The value itself isn't recursively constant, only the view of the value is immutable, i.e. in your example you can't modify o
from c
. It's an "immutable reference." Naming is up for discussion.
Solving problems with concurrent code will take more than this and is beyond the scope of this proposal, but I think this is a step in the right direction.
I don't really like these wrapper types, mostly because Null<T>
is such a mess. On the other hand, I'm not sure how else to handle something like this because our internal type instance handling is a mess as well.
Provides a way to create a recursive read-only view of a value.
Just want to note that as I went to submit this, I noticed an "immutable" branch which has a nearly identical proposal from @nadako, down to the examples used. I didn't know about or reference @nadako's proposal in constructing this one...great minds think alike I suppose :wink: In fact I came to this idea over the past several days when thinking about a few concrete use cases, including one in HaxePunk: https://github.com/HaxePunk/HaxePunk/pull/574 and tried out a macro solution which wasn't totally satisfactory.
Thought I would submit anyway to get the ball rolling.
Rendered version