-
Notifications
You must be signed in to change notification settings - Fork 59
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 a Const<T> immutable wrapper type #41
Conversation
Here's a basic prototype: https://github.com/bendmorris/haxe/tree/const |
What is the advantage of this over the |
The use cases are pretty different. |
I think that simply applying Const to an object will not be able to do as you desire. Yes, for Array and Map, we can disallow use of functions that may modify the underlying object, but that cannot be enforced for random methods on generic objects, unless you now add some new metadata to each method that describes the enforcement policy. (I'm pretty sure that people want to avoid "'const' hell" as it is known in C.) 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 would be amazed if the typer looked any further than the current method when performing its function; certainly it doesn't follow deep code paths.) That is, unless I'm misunderstanding the whole concept here. |
Is Const needed for Ocaml target? |
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.
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. |
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
This proposal adds a third option: return the list as a |
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.
Const also prohibits reassignment. It's not orthogonal if they do the same thing. 😄 However, 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.
Ah, the bugs that will be generated! If it's not exhaustive, it's a maintenance headache.
Correct, it was added as a similar (opt-in) concept to C89, almost 30 years ago. And engineers have hated it ever since.
In your example above, a Const<List> is returned -- but you want the Const-ness to apply to SomeType (and its methods and return values) recursively without explicitly declaring it. (After all, you're passing back a list that is active -- and not const -- in another context.) So how is the compiler supposed to know that there should be an error here?: 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 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."
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.
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.
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: |
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 as well. (And I think you intend them to be.) However, that breaks interoperability between libraries, doesn't it? Suddenly, you either a) can't use a companion library that doesn't conform (because the compiler errors at the call point), or b) you cast away or subvert all of that const-ness and break the library writers assumptions. (Believe me, you will get support calls from people who break your assumptions.) And, then, for interoperability's sake, everybody has to conform and it's no longer optional. |
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 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. |
The value itself isn't recursively constant, only the view of the value is immutable, i.e. in your example you can't modify 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 |
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 😉 In fact I came to this idea over the past several days when thinking about a few concrete use cases, including one in HaxePunk: HaxePunk/HaxePunk#574 and tried out a macro solution which wasn't totally satisfactory.
Thought I would submit anyway to get the ball rolling.
Rendered version