Feature Request : 'With' statement
Like in Pascal, or Javascript, or D...
Code like this seems so unnecessarily verbose :
...
canvas.Scissor=New Recti( sx,sy,sx+sw,sy+sh )
canvas.Clear( New Color( 1,32.0/255.0,0,1 ) )
canvas.PushMatrix()
canvas.Translate( tx,ty )
canvas.Scale( Width/640.0,Height/480.0 )
canvas.Translate( 320,240 )
canvas.Rotate( Millisecs()*.0003 )
canvas.Translate( -320,-240 )
...
Isn't this so much cleaner :
...
With canvas
.Scissor=New Recti( sx,sy,sx+sw,sy+sh )
.Clear( New Color( 1,32.0/255.0,0,1 ) )
.PushMatrix()
.Translate( tx,ty )
.Scale( Width/640.0,Height/480.0 )
.Translate( 320,240 )
.Rotate( Millisecs()*.0003 )
.Translate( -320,-240 )
End WIth
The above is a fairly trivial example to simply avoid some typing but it can be oh so much more powerful :
With SlowComplexFunctionToLookupAnObject()
.DoThing()
.DoOtherThing()
... do lots of things with the object you looked up, without having to create a local variable to hold it
End With
Or :
With New ThingThatNeedsComplexInitialisation()
.BuildFoundations()
.AddRoom("Home", 2, 2)
.AddDoor(2,2,2,3)
.AddCorridor(5,5,5)
...build whole house, or whatever
End With
Ah, yes! We were having a discussion about this particular feature request in Discord!
Some users recommended even doubling it as some sort of anonymous initializer block( I think Ethernaut and Nerobot ) and I also loved the ideas.
I am definitely a supporter of the With statement and a feature I would be ASTOUNDED(!) to see would be for it to be doubly usable as a loop! Something like a terse "for each with" loop.
Example of With doubling as a loop
Field dudes:Stack<Person> ' dudes is an iterable container of a Person
Method ExplodeInRadius( point:Vec2i, radius:Int )
With dudes ' (or maybe -> With Eachin dudes )
If position.Distance( point ) < radius Then ' position is a field of Person
Kill() ' Person has a method to kill them
End
Next
End
As you can see, it's essentially an awesome way to go through a Class/Structure Iterable and use the code-block in between as if it's the scope of the current iterated object! I used to use this feature all the time in another language and would be so happy to use it in Monkey2!
This seems for a great place to have a Dispose() method called, since ThingThatNeedsComplexInitialisation() goes out of scope at "End With". In C#, "using" is a great way to have short lived objects, that needs cleaning up when done, such as file handles or factories. https://stackoverflow.com/questions/75401/what-are-the-uses-of-using-in-c-sharp
The only criticism of With statements is that they make it difficult to read the code because it is difficult to tell at a glance which function will be invoked if it is called within a With block (is it a method of the With object? Is it a method of the current object scope? Is it a global...). In all of my above examples, I have prefixed all methods of the With-scope object with a dot. This makes the intention clear and also avoids any With-scope methods shadowing any Local-scope methods. I don't know if a similar dot exists in any other language but it seemed to me to be a good idea.
@abe-noll, yes that sound like a great idea, basically a shorthand for :
For Local dude := Eachin dudes
With dude
If .position.Distance(thing) > radius Then dothing() Else .Kill() ' (see how those dots help :)
End With
Next
@Difference, 'using' in C## (according to what I read behind your link) seems to be a shorthand for a try-catch block and still requires instantiation of a local variable. That local variable is something I was hoping to avoid with a With block (remember that dot prefix :). In your opinion, when should the Dispose() method be called?
- When an exception is thrown? (Use a try-catch block instead)
- When you Return() from within a With block? (why would you do that?! It's asking for trouble!)
- Simply at the end of the WIth block when the 'nameless' local variable goes out of scope? (in which case call .Dispose() just before End WIth). I am not a fan of functions being called by some implicit background process. If you want to call a Dispose-like method, you should do it explicitly :
WIth OpenFIle(filename)
While Not .EOF()
Print(.ReadLine())
End While
.CloseFile() 'Explicit 'disposal' of the file handle
End With
``` .
But perhaps I misunderstood your suggestion? Do you have a real-life example?
Well @DoctorWhoof (Ethernaut) suggest actually doing it inside of the With statement like so:
With dude := New Person
name = "Grommit"
age = 34
dog = True
Print IsAlive()
End
Regarding With Scope
I believe the With statement should be defined in a clear way, then anyone learning the language wouldn't need to be confused with what's being done inside a With block.
- Anything inside of the
Withblock should only be limited to 3 scopes: theWithapplied instance, the local level (local variables and method parameters), and the global scope.- If the caller is an instance you should reference it using a local variable ( Or perhaps a keyword property called
Otherwhich is similar toSelfbut relevant forWithblocks )
- If the caller is an instance you should reference it using a local variable ( Or perhaps a keyword property called
- The access level of a
Withblock, over the applied instance, should be limited to what memory is allowed to be accessed as usual over the instance.- If you can access a certain privacy level using the DOT accessor then that same access should be NO different for the
Withblock.
- If you can access a certain privacy level using the DOT accessor then that same access should be NO different for the
@abe-noll I'm afraid you've lost me somewhat. @DoctorWhoof suggested doing what? inside the With statement? I agree that there are only three relevant scopes - Global, Local and With-block (unless we are also within a Try block or a For loop...). My argument for using a dot prefix is nothing to do with privacy. It is simply to avoid With-scope methods shadowing Local-scope methods, to make intent absolutely clear without having to think about it. It seems like a natural thing to do if you compare this :
dude := New Person()
dude.name = "Grommit"
dude.age = 34
dude.dog = True
Print(dude.IsAlive)
with this :
dude := New Person()
WIth dude ' semantically, this means "if you see a dot, stick 'dude' before it"
.name = "Grommit"
.age = 34
.dog = True
Print(.IsAlive)
EndWith
But, don't forget that it is not even necessary to create a local named variable (ie. 'dude'). The point of a With block is to allow access to methods (and fields and properties...) of an unnamed object within the With block. In other words, your example could simply read (without my dot prefixes) :
With New Person()
name = "Grommit"
age = 34
dog = True
Print IsAlive()
End With
But in the above code the new person would be GC'd as soon as the With scope was exited unless the Person constructor registered a reference within a wider scope. This would make more sense using a Factory (ie. With CreatePerson() instead of With New Person() )
I don't like the idea of yet another scope keyword (such as Other). Couldn't 'Self' be used within a With scope to reference Local scope methods? Or, instead of that, perhaps we could prefix With-scope calls with a dot? That way it is immediately obvious if a call is on the With-scope instance or intended for some wider scope. And probably easier for Mark to implement as there is never any ambiguity. But I fear I am repeating myself :)
I guess the issue gets rather muddier if you consider nested With blocks... how do other languages deal with this?
@arpie42 I'm saying that the scope of the With statement should not be able to use functions, methods, properties, or fields of two different objects. As is common with other languages. This causes the confusing predicament of naming conflict precedent in which it becomes a challenge to the programmer to look at the code and assume what the coder's intent was.
I know you had a solution which was just putting a dot for the With instance but, like you mentioned, nesting with statements becomes even more challenging. That's why I proposed not to let a With statement have access to any scope but whatever's limited to Local varname:= and globals.
My proposal for variable scope of With statement
A demonstration of what is valid and what isn't valid
Global peopleInBathroom:Int=0
Class House
Field bathrooms:Int
Method SetHomeOwner:Bool( person:Person ) Abstract
Const MinimumBathrooms:=1
Method EnterBathroom:Person( persons:Person[] )
Local bathroomsLeft:= bathrooms
Local house:=Self
With persons ' With loop!
If IsAThief() Then ' ALLOWED: method of person
'SetHomeOwner( Self ) ' ERROR: SetHomeOwner not in persons scope
End
'If Not _wantsToUseBathroom Continue ' ERROR: Private variable not allowed from House method
' (if we were inside of a Person class the line above is valid)
If Not HasToUseBathroom() Continue ' ALLOWED: method of Person
If bathroomsLeft<=0 ' ALLOWED: Local variable, above
'Print String(bathrooms) +" is too little! " ' ERROR: bathrooms not in person's scope
Return Self ' ALLOWED: Self is the current "Person" in persons
Endif
peopleInBathroom += 1 ' ALLOWED, global access
bahtroomsLeft -= 1 ' ALLOWED, local variable
Print "Thank goodness you have " + house.bathrooms + "!" ' ALLOWED: access house via local
Next
End
End
Regarding what DoctorWhoof said
An additional specification for the with statment should allow us to have a local variable stored in a name in the With header itself. Acceptable usage example:
' CASE: Normal Local variable
Local person:=New Person()
With person
' do with person
Return person
End
' CASE: Integrated local variable
With Local person:=New Person()
' do stuff
Return person
End
' CASE: Unamed object
With New Person()
' do stuff
Return Self
End
Okay, I think we're both making this over-complicated. What we are proposing is just a load of syntactic sugar for things that can already be done other ways in Monkey. All this talk of different scopes and dot-prefixes is just messy.
What should be available within the scope of a With block? It should behave exactly the same as a For-Next block or a While-Wend block or a Repeat-Until block. We all know how they work already.
Having been pondering on this all day, I have concluded that introducing a new keyword simply to avoid a bit of repetitive typing is not a convincing argument. A new keyword needs to add something currently impossible, or solve a problem that exists with the language.
The proposed With-Eachin-Next loop introduces an unnamed variable and, with it, potential confusion. I don't see what it adds over this currently perfectly good code :
Field dudes:Stack<Person> ' dudes is an iterable container of a Person
Method ExplodeInRadius( point:Vec2i, radius:Int )
For Local d := Eachin dudes
If d.position.Distance( point ) < radius Then 'There is no doubt that position is a field of Person
d.Kill()
End
Next
End
Also, @abe-noll , while your bathroom thief example was great for demonstrating your arguments for what should or shouldn't be in scope, surely nobody would actually write code like that?! If they do, I don't want to be around when it needs debugging!
Am I retracting my request for a With block? No. There is a problem that I was hoping to solve. There is something that is currently not really possible in Monkey.
The Problem
The problem is this: I want to create several similar complex objects. For each object I want to be able to call several methods to set various parameters. For each object I create I therefore need a temporary variable to store it in while calling its build methods. If I only want to create one person, it is easy :
Local p := New Person()
p.name = "Grommit"
p.age = 34
p.dog = True
But if I then want to create another person, perhaps a different subclass of person, I would want to continue with this :
Local p := New EvilPerson() 'This will cause a compiler error : duplicate identifier 'p'
p.name = "Evil Penguin"
p.age = 15
p.dog = False
Basically I want to be able to reuse a temporary variable for creating multiple entities, components, people, guiElements, whatever. Of course, I could use a different name for each one, but this would litter the Local scope with variables that I will only use once, and that is just messy.
The Solution?
I want to be able to write something functionally equivalent to this (but with semantic keywords!):
Repeat 'Im not actually repeating anything - this is just to create some local scope!
Local p := MakeNicePerson()
p.DonWig()
p.ChooseCasualClothes()
p.GroomMoustache()
Until True 'p goes out of scope here
Repeat
Local p := MakeEvilNemesis() 'So I can reuse p here!
p.ReloadGun()
p.RepairArmour()
p.LaughManically()
Until True
This is pure abuse of a Repeat-Until loop but does achieve exactly what I am looking for. Another way to look at it might be as a For-Eachin-Next loop with one solitary object instead of a collection.
So I have changed my mind about the introduction of a nameless variable as that just seems to cause confusion. I would be happy to just name a local variable with an underscore or a single letter. Lets keep it simple :
With Local p := MakeNicePerson()
p.DonWig()
p.ChooseCasualClothes()
p.GroomMoustache()
End With
With Local p := MakeEvilNemesis()
p.ReloadGun()
p.RepairArmour()
p.LaughManically()
End With
@abe-noll On a completely different note, can you tell me how I switch on syntax-highlighting in my code examples, like you have done?
Why I support a limited scope spec
My recommended scope is just a solution to a common problem found in With statements - that problem being With-blocks have mixed scopes and cause name confusion! It doesn't change scope at all, it just defines the scope of the block to something clear and unambiguous. If we're going to be working with a Person then we should only be able to directly use whatever a person can! In my opinion, because this is just an opinion, preventing a With-block from having mixed-access is crucial! If you were normally to work with a Person method you'd never find it trying to access another object's members unless explicitly using a local variable - it should be no different for a With-block.
Regarding your specified problem
Your problem comes down to scope limitations; the With statement solves a different problem. If we were using another language, With would not be the answer to being able to use the same variable name for differently typed instances. Though, if you had the same types you could reuse your variable name, which is no problem, but, if they're differently typed, then you really should name them differently for the sake of ambiguity. In another language, you'd just be able to define a new block-scope by using curly-braces directly, or similar, and that solves the problem! In my opinion, your problem comes down to scope, like "begin-end", or coding habits. But that doesn't mean you shouldn't be able to use With statements as a syntactic sugar for what you want!
' Something more specific to your problem found in Delphi
Begin Local strings:=New StringStack ' or just Begin
' ....
End
Begin Local strings:=New StringList
'....
End
(I also just notice the .notation is found in Basic, which I have no experience with, and assume this might be why you are in favor of that style)
What With really comes down to
In the end, With is syntactic sugar. I've found this to be true in how other languages operate and even languages, like Python, where With is something like a "use-this-one-time" are still using With as syntactic sugar for a try-finally sort of deal.
Syntactic sugar solves a problem: readability + progress
Syntactic sugar has never been a bad thing and it certainly was never an un-appreciated thing. My suggestions and the With statement, overall, do three things:
- The proposed scope limitation fixes any ambiguity problems
Withmakes the program shorter => easier to read, write and refactor => cognitive benefit- Offers a way to work with 1 or many objects in one block: This is progressive, expressive, and can benefit refactoring
Other languages, like JavaScript or Java, could just keep letting people do certain things the old way and forget about syntactical-sugar. Though, when they do add new language features which are originally labeled as "sugar" those sugars start becoming the idiomatic way of doing things! I think With fits that argument very well and is really something Monkey2 is missing!
Monkey2 has mix of readibility, brevity and directness. I think With ticks all of those checks being that it has a readable intent, it makes your code more brief, and the application of the statement is direct. You want to do something With some instance(s).
@arpie42 You should join us in the Discord chat! I'm literally always around and so are a lot of other Monkey-Users! Regarding your question, take a look at this: syntax highlighting in Github
I mostly agree with all of your points. I think all the relevant arguments have been made in this already enormously long discussion over what I thought was a trivial request! I guess we now have to hope Mark has taken the time to read through it all, and the WIth statement will magically appear in the next release!
I would be very happy to see the addition of a With statement in Monkey, and would be happy for it to have a nameless object instance whose members are accessible, or not. Either way works for me.
As for your limited scope suggestion, I see where you are coming from and mostly agree with you but I fear this would be too limiting - I would want to be able to also access members of the object containing the code (the scope immediately outside the With block), perhaps with another new keyword (Outer?) as you suggested earlier on. But I fear that is adding too much complexity for such a (seemingly) simple concept.
Yes, my particular problem comes down to scope and your Block-End suggestion would solve it. So would a With block, which could also add various other benefits and will be more familiar as a concept to most people (or maybe it wouldn't, what do I know!?).
I would also be able to solve my problem using method chaining (most of the time) but I've never been a big fan of methods that return Self - it is hard to maintain consistency. And, anyway, it is cumbersome to do method chaining in Monkey because of it's limited line-break rules : you cannot break a line ending or beginning with a dot, you can only break on a comma within a parameter list, which makes method chaining really ugly. Anyway, that's where my dot prefix suggestion came from :) The only Basic I am familiar with is BBC Basic on the Acorn Electron, and later on RISC OS, but I don't think anybody uses those any more and they certainly don't have any dot notation!
I will reiterate one of your earlier points - the With block, if we ever get it, needs to be clearly and unambiguously documented, especially if its scope rules are different from what we are used to from other blocks. We are already very familiar with scope in loops, try-catch, if-then, etc. and if With does it differently then that needs to be made absolutely clear.
I will check out the Discord channel (never used Discord before!) and thanks for the link on syntax highlighting.
@arpie42 : I'm not advocating the need for a typed local variable. If the With syntax is implemented, I assume it's because you wan't to avoid that, as you already described.
I do however like the possibility that an object can do it's own cleanup, be it at an end of a With block or because a Try Catch Block, stops further execution.
I guess my idea has more to do with having an iDisposable Interface (with a Dispose() Method) that, if implemented, Monkey2 calls when an object goes out of scope.
@Difference If I've understood correctly you are proposing a Dispose() method that is automatically called when an object goes out of scope? Kind of like a destructor for non-GC'd languages (but without any memory management to do)? The only use case I can think of is for closing files but I imagine there are many other uses. Then again, I'm a fan of explicit coding; methods that get called automagically make me slightly uneasy. In any case, surely this sort of functionality needs to be implemented for all cases of an object instance going out of scope, for all scoped blocks? Why would it be any different or more relevant for a With block?
@arpie42 : To me With...End is different, because it defines exactly when the object goes out of scope, I see it as the point of that syntax, if you disregard the brevity advantages. I think Mark made it clear that general Destructor methods are not in the pipeline for Mx2, but I still like the idea that you can have a "whatever happens in Vegas, stays in Vegas" block. Yes, I'd probably use it for files, and database connections, especially handy if blocks are nested, and something deep down throws an error.
I think I would still argue that a try-catch block is more appropriate for the uses you are describing, @Difference, but it is fascinating that what I thought was a simple request for a With block has turned up so much discussion. I would be delighted if Mark were to introduce any of the implementations we have talked about. I have total confidence that whatever he does will be the right thing.
I don't agree with @Difference about the with statement here. As it stands, memory management is not a part of the Monkey2 paradigm and I don't see an anonymous With as the answer.
The problem here is unrelated to a block-scope keyword in my opinion. The suggestion to use With as the one block we're allowed to use to specify the exit of an object is no different than explicitly calling a cleanup function because, ultimately, we don't have any control over memory to begin with. And if I'm catching what you're saying correctly ... there wouldn't be much of a difference between these two:
' Doing it via scope
With New Object
' do stuff and cleaned
End
' Directly
Local obj:=New Object
obj.Clean()
I think you are both correct, my suggestion should not be limited to With...End.
It should be a Dispose(), Finally() or OnExit() that is called, when the object goes out of scope. I suggest, that it could be limited to objects that implement an iDisposable interface.