(not) handling new programming idioms with grace

classic Classic list List threaded Threaded
95 messages Options
12345
Reply | Threaded
Open this post in threaded view
|

Re: (not) handling new programming idioms with grace

William Ahern
On Wed, Jul 18, 2018 at 12:55:04PM -0300, Roberto Ierusalimschy wrote:

> > On 18 July 2018 at 15:20, Roberto Ierusalimschy <[hidden email]> wrote:
> > > I did not get your point here. Why a user that would forget to properly
> > > qualify its 'local' would not forget to use 'with'? What are the
> > > differences between 'local scoped' vs 'with' except the syntax?
> >
> > it's precisely the syntax.   a 'with' block is always an extra block,
> > even if it covers the whole loop or function block (it usually
> > doesn't).  it's very visible, and so hard to miss.   if a novice
> > programmer sees the `with open('fname') as file: xxxx`  and likes it,
> > they will probably use it very intentionally.
>
> It is not by chance that Lua avoids too many syntactical constructs.
> They are hard to be represented in the C API.
>
> A good use I see for "local scoped" is in C functions, which often have
> a hard time to properly free resources. With something like "local scoped",
> it would be enough one single function to mark a stack position as
> a scoped variable, to be finalized when the function exits.
>
> -- Roberto

That's intriguing.

I wish people (myself included) would keep in mind the symmetry that Lua
tries to maintain between the language and the C API. Aside from making the
C API incomparably superior to other language implementations, the
constraint keeps Lua honest and careful. If a feature can be elegantly
expressed and implemented for *both* the language and the C API, that's
strong evidence it's a quality choice. If it can't be, it suggests to keep
searching.

The filter may not have a great false negative rate, but the false positive
rate is where it really produce dividends. Many proposals on the list could
be and should be easily dismissed amicably by applying this filter without
us having to be reminded.

Reply | Threaded
Open this post in threaded view
|

Re: (not) handling new programming idioms with grace

Gé Weijers
In reply to this post by Roberto Ierusalimschy


On Fri, Jul 20, 2018 at 9:34 AM Roberto Ierusalimschy <[hidden email]> wrote:
I am still curious whether that could work as I suggested, calling
__gc as a kind of emergency collection, only when some resource
allocation fails. For instance, in the case of file handles in Linux,
that would mean ~1 full GC for every 1000 streams created, not
one GC per stream.


If Lua is embedded in a larger program that uses files or other types of resources elsewhere you end up having to call the emergency Lua GC from who knows where in the code. This may be a 3rd party library, or even a system shared library/DLL that you have no control over. A failure to allocate a file handle somewhere deep in library code may be unrecoverable.

It's not something I would want to rely on. I have ended up coding a version of the Lisp unwind-protect construct to handle this issue. It's not perfect because you lose error information when you 'rethrow' an error after releasing the resource, and you have to define two local functions in the Lua version.


--
--

Reply | Threaded
Open this post in threaded view
|

Re: (not) handling new programming idioms with grace

William Ahern
On Fri, Jul 20, 2018 at 12:05:33PM -0700, Gé Weijers wrote:

> On Fri, Jul 20, 2018 at 9:34 AM Roberto Ierusalimschy <
> [hidden email]> wrote:
>
> > I am still curious whether that could work as I suggested, calling
> > __gc as a kind of emergency collection, only when some resource
> > allocation fails. For instance, in the case of file handles in Linux,
> > that would mean ~1 full GC for every 1000 streams created, not
> > one GC per stream.
> >
> >
> If Lua is embedded in a larger program that uses files or other types of
> resources elsewhere you end up having to call the emergency Lua GC from who
> knows where in the code. This may be a 3rd party library, or even a system
> shared library/DLL that you have no control over. A failure to allocate a
> file handle somewhere deep in library code may be unrecoverable.

The same applies to malloc failure. Should Lua also not bother invoking the
emergency collector when malloc fails?

Poor library QoI doesn't justify crippling the runtime or better written
libraries. Rather, the feature should stand on its own merits.

> It's not something I would want to rely on. I have ended up coding a
> version of the Lisp unwind-protect construct to handle this issue. It's not
> perfect because you lose error information when you 'rethrow' an error
> after releasing the resource, and you have to define two local functions in
> the Lua version.

You don't have to rely on it. The nice thing about emergency collection is
that it doesn't really change language semantics or behavior; just fewer
people are accidentally bitten. If it works well enough that the failure to
aggressively deallocate resources results in fewer bugs than all the other
bugs in someone's application, it's a win. The calculus would be different
if implementing the behavior had real costs in terms of language semantics
or implementation complexity, but those costs are effectively nil.

Ideally we'd get a construct that allows us to elegantly request immediate
destruction of particular resources. Emergency collection doesn't make that
construct less likely to emerge unless the Lua authors would be contented by
emergency collection. That's not my impression but maybe I'm wrong about
their motivations. In any event, let's not let the perfect be the enemy of
the better.

Reply | Threaded
Open this post in threaded view
|

Re: (not) handling new programming idioms with grace

Hisham
In reply to this post by Roberto Ierusalimschy
On 20 July 2018 at 13:33, Roberto Ierusalimschy <[hidden email]> wrote:

>> > I remain curious. Has this been tested and found too slow before?
>>
>> Yes. I can't provide a reference right now but relying on
>> collectgarbage() to force release of non-memory resources via __gc at
>> the necessary times has proven to be unusable for my workload pattern.
>> It incurred a significant performance hit due to unnecessary memory
>> traversal.
>
> I am still curious whether that could work as I suggested, calling
> __gc as a kind of emergency collection, only when some resource
> allocation fails. For instance, in the case of file handles in Linux,
> that would mean ~1 full GC for every 1000 streams created, not
> one GC per stream.

For LuaRocks traversing a directory that would have been okay, but for
any kind of resource being used by a server, doing a full GC every N
requests (whether N = 100, 1000 or 10000) would produce pretty
noticeable spikes in the performance graphs. We're talking about
significantly degraded performance (a full stop-the-world GC that
wouldn't ever otherwise happen given incremental/generational GC,
right?) on one out of every N requests. I imagine that running a full
GC during a game frame would also be frowned upon.

Emergency-collecting on io.open hides the problem in that one
particular case which happens to be the easy-to-understand go-to
example for advocating deterministic resource release, but it doesn't
sound like an approach that one would want to do on other, even more
limited resources, such as DB connection handles, or ones that should
always be released ASAP even if the resource isn't exhausted, such as
sockets — one might argue that sockets should always be explicitly
closed and never left to the GC, but then libraries such as LuaSocket
follow the example of Lua's io library and implement __gc for sockets,
as if to say that this is okay.

If the emergency collection became the standard behavior of io.open I
suspect that "beware that opening a file in Lua may trigger a full GC"
would become one of those dreaded folk-wisdom performance tips.

-- Hisham

Reply | Threaded
Open this post in threaded view
|

Re: (not) handling new programming idioms with grace

Hisham
In reply to this post by Roberto Ierusalimschy
On 18 July 2018 at 15:55, Roberto Ierusalimschy <[hidden email]> wrote:

>>   A few months ago I wrote some code to recursively dive into directories,
>> using something like:
>>
>>       [...]
>>
>>   I was exhausting file descriptors before my program finished [1].  Upon
>> investigation, it was because iterating over a directory uses a file
>> descriptor, and because GC had yet to kick in, I had a bazillion open
>> directories.  I had to change the iterator to close and release the
>> directory upon finishing instead of relying upon the GC.  And this is in
>> what I consider library code.
>
> I think a few rules could greatly improve things. Unfortunately, I was
> not following them :-)
>
> - As Tomas pointed out, iterators should release their resources as soon
> as they reach the last element. Sadly, my implementation of a directory
> iterator in PiL does not do that; but the implementation of io.lines in
> the standard library does :-)

The implementation of lfs.dir in LuaFileSystem does close the
directory handle as it reaches the last element. Unfortunately, that's
not good enough, because of course, when given a "for" iterator, users
can (and do) use "break".

-- Hisham

Reply | Threaded
Open this post in threaded view
|

Re: (not) handling new programming idioms with grace

John Belmonte
In reply to this post by Roberto Ierusalimschy
From the land of PEP: deterministic cleanup for iterators


Can the prospective scoped locals be applied to this use case?   I guess the iterator would need to be implemented by coroutine, with the scoped value passed to resume()?


On Thu, Jul 19, 2018 at 12:19 AM, Roberto Ierusalimschy <[hidden email]> wrote:
> If Roberto wants to go with a scope-qualified local, at least it's a
> slippery slope to laying a new control structure on top of it :)
> Though it will be important that exit is called on the locals in the
> reverse order of declaration.

We are quite familiar with these "slippery slopes". It is in fact a
hard argument to not add scope-qualified local. We usually add a
feature to solve a problem, not to create new ones.

We had something (scope-qualified locals) that seemed to be enough.
Now we have this list of problems:

- It does not create a new scope by itself;
- It is not visible enough;
- It still does not solve the problem for global variables;
- It cannot supress exceptions.


-- Roberto


Reply | Threaded
Open this post in threaded view
|

Re: (not) handling new programming idioms with grace

Roberto Ierusalimschy
> >From the land of PEP: deterministic cleanup for iterators
>
>   https://www.python.org/dev/peps/pep-0533/
>
> Can the prospective scoped locals be applied to this use case?

No. The proposed 'with' clause also does not seem to apply :-)

-- Roberto

Reply | Threaded
Open this post in threaded view
|

Re: (not) handling new programming idioms with grace

Roberto Ierusalimschy
In reply to this post by Hisham
> The implementation of lfs.dir in LuaFileSystem does close the
> directory handle as it reaches the last element. Unfortunately, that's
> not good enough, because of course, when given a "for" iterator, users
> can (and do) use "break".

That is true. And directory iterations are frequently broken (e.g.,
after finding something). However, as just pointed out in another
thread, none of the proposed mechanisms for deterministic finalization
would be practical for that case, because the variable that needs
finalization is hidden inside the for (or worse, hidden inside the
closure or coroutine used by the for).

-- Roberto

Reply | Threaded
Open this post in threaded view
|

Re: (not) handling new programming idioms with grace

Roberto Ierusalimschy
In reply to this post by Hisham
> If the emergency collection became the standard behavior of io.open I
> suspect that "beware that opening a file in Lua may trigger a full GC"
> would become one of those dreaded folk-wisdom performance tips.

In a generational collector, any allocation can trigger a major
collection. io.open allocates memory (to create the stream handle),
so this dreaded folk-wisdom performance tip is bound to happen anyway.
(Or maybe not?)

-- Roberto

Reply | Threaded
Open this post in threaded view
|

Re: (not) handling new programming idioms with grace

John Belmonte
In reply to this post by Roberto Ierusalimschy
In the generic for statement pseudocode, could explist return a 4th, optional result which is assigned to a scoped variable?  I assume a scoped var with value nil is a no-op on exit.

    do
       local f, s, var, exit_f = explist
       local scoped _exit_f = exit_f
       while true do
         local var_1, ···, var_n = f(s, var)
         if var_1 == nil then break end
         var = var_1
         block
       end
     end

On Sat, Jul 21, 2018 at 7:55 PM, Roberto Ierusalimschy <[hidden email]> wrote:
> >From the land of PEP: deterministic cleanup for iterators
>
>   https://www.python.org/dev/peps/pep-0533/
>
> Can the prospective scoped locals be applied to this use case?

No. The proposed 'with' clause also does not seem to apply :-)

-- Roberto


Reply | Threaded
Open this post in threaded view
|

Re: (not) handling new programming idioms with grace

Roberto Ierusalimschy
In reply to this post by William Ahern
> Ideally we'd get a construct that allows us to elegantly request immediate
> destruction of particular resources. Emergency collection doesn't make that
> construct less likely to emerge unless the Lua authors would be contented by
> emergency collection. That's not my impression but maybe I'm wrong about
> their motivations. In any event, let's not let the perfect be the enemy of
> the better.

Even without any new construct, the idea of "emergency collections"
is not intended to be the basic method of resource release. We still
believe that programmers should call explicit 'close' methods after
using a resource.  Only in exceptional circunstances (mostly when
exceptions occurr) the regular 'close' method would not be called.
Even then, regular garbage collection would deal with most of these
leftovers. I would expect that in a well-written program, "emergency
collection" would be really rare, but we have no data on this.

Moreover, it is worth keeping in mind that we are talking about
operations that can still fail honestly (e.g., your program really may
have too many open files, or worse, the system has too many open files,
errno ENFILE).  If the program can sustain such failures, it probably
can sustain a full GC cycle---with less damage. It is also worth keeping
in mind that real emergency GCs can happen at every single allocation
(e.g., in an assignment to a table field), so it should not be such a
big deal.

Finalization of iterators is a different matter. Unlike regular stuff,
it seems that programs can skip their finalizers (by breaking the loop)
in non-exceptional circunstances, and there is not much programmers can
do about that currently. As pointed out, none of the proposals being
discussed would solve that. Maybe the proposal just made by John
solves that.

-- Roberto


Reply | Threaded
Open this post in threaded view
|

Re: (not) handling new programming idioms with grace

Weird Constructor
2018-07-21 18:56 GMT+02:00 Roberto Ierusalimschy <[hidden email]>:
> Even without any new construct, the idea of "emergency collections"
> is not intended to be the basic method of resource release. We still
> believe that programmers should call explicit 'close' methods after
> using a resource.  Only in exceptional circunstances (mostly when
> exceptions occurr) the regular 'close' method would not be called.
> Even then, regular garbage collection would deal with most of these
> leftovers. I would expect that in a well-written program, "emergency
> collection" would be really rare, but we have no data on this.

If the rule is, that explicit resource freeing is better than implicit, why
implement a GC at all? It's the programmers fault for not freeing memory.
And a GC is not only implicit, but it's basically an unpredictable
non-deterministic operation in a program.

I would really like if there was some (meta-)method being called upon
a "regular"
stack unwind, like "return" or "error". I would not expect a "yield" to free
my file handle. Like in C++ it would allow for some more useful implicit
operations that the programmer could formulate in his programs.

It's standard in C++ to use a std::lock_guard() to make a clear point.
To tell everyone who reads the code: "don't worry, if this routine is done,
this mutex is released". Something like that is really missing from Lua and
can't be implemented from inside Lua. I would not expect a full blown
"(unwind-protect ...)" like in Lisp here, but leaving a mark on the stack
like you suggested earlier can do the trick.

An argument would be, that it would break TCO in an implicit and/or
unexpected way if it is not explicit that things are to be closed upon
scope exit.

  function read_config(x)
      auto f = io.open(...)
      --- ...
  end

I see that it introduces a new implicit concept, that non regular
programmers might have their issues with. Is that what you worry
about?


Greetings

Reply | Threaded
Open this post in threaded view
|

Re: (not) handling new programming idioms with grace

John Belmonte
In reply to this post by John Belmonte
Regarding the function signature of scope exit:  since it's possible to throw value nil it seems like it will have to be (success, error_value).

On Sat, Jul 21, 2018 at 11:22 PM, John Belmonte <[hidden email]> wrote:
In the generic for statement pseudocode, could explist return a 4th, optional result which is assigned to a scoped variable?  I assume a scoped var with value nil is a no-op on exit.

    do
       local f, s, var, exit_f = explist
       local scoped _exit_f = exit_f
       while true do
         local var_1, ···, var_n = f(s, var)
         if var_1 == nil then break end
         var = var_1
         block
       end
     end

On Sat, Jul 21, 2018 at 7:55 PM, Roberto Ierusalimschy <[hidden email]> wrote:
> >From the land of PEP: deterministic cleanup for iterators
>
>   https://www.python.org/dev/peps/pep-0533/
>
> Can the prospective scoped locals be applied to this use case?

No. The proposed 'with' clause also does not seem to apply :-)

-- Roberto



Reply | Threaded
Open this post in threaded view
|

Re: (not) handling new programming idioms with grace

John Belmonte
Thinking ahead about some conventions related to scoped variables:

Libraries and classes will have functions intended for use with scoped vars.  They will acquire some resource or otherwise initialize state, returning an exit function.

It would be good to have a naming convention from the onset, so it's clear when an API is meant for scoped vars.  Perhaps the prefix "scoped".

   local scoped _ = my_lock:scoped_acquire()

or if the semantics are obvious, simply:

   local scoped _ = my_lock:scoped()

There is the question about how to represent errors in acquiring the resource.  In this example it's an error if the lock is already active.  It's strongly preferred to use exceptions for errors.  Given that scoped vars are tied to exception safety, it's reasonable to assume exceptions will be acceptable to users of the library.

The alternative of returning "nil, error" is problematic:

  local scoped l, err = my_lock:scoped()

The issues being that 1) it's wasteful to used a scoped var for err, 2) if err happened to be a callable then unexpected behavior would ensue.  #2 could be due to some lower level dependency out of the library author's control.
Reply | Threaded
Open this post in threaded view
|

Re: (not) handling new programming idioms with grace

John Belmonte
In reply to this post by Roberto Ierusalimschy
On Wed, Jul 18, 2018 at 11:55 PM, Roberto Ierusalimschy <[hidden email]> wrote:
It is not by chance that Lua avoids too many syntactical constructs.
They are hard to be represented in the C API.

If "with" is syntax sugar on top of scoped vars then a representation in the C API isn't needed.

The benefits of "with" syntax are related to readability:  1) it's clear by code structure that a resource is open, and 2) named variables can be elided in many cases, e.g.:

    with my_lock:scoped() do ... end

Proposed pseudocode for "with":

A with statement like

     with exp_1, ···, exp_n do block end

is equivalent to the code:

     do
         local scoped var_1, ···, var_n = exp_1, ···, exp_n
         block
     end

where var_i are invisible.

with ... as statement like

     with exp_1, ···, exp_n as var_1, ..., var_n do block end

is equivalent to the code:

     do
         local scoped var_1, ···, var_n = exp_1, ···, exp_n
         block
     end

 
Reply | Threaded
Open this post in threaded view
|

Re: (not) handling new programming idioms with grace

Patrick Donnelly
In reply to this post by Roberto Ierusalimschy
On Sat, Jul 21, 2018 at 4:55 AM, Roberto Ierusalimschy
<[hidden email]> wrote:
>> >From the land of PEP: deterministic cleanup for iterators
>>
>>   https://www.python.org/dev/peps/pep-0533/
>>
>> Can the prospective scoped locals be applied to this use case?
>
> No. The proposed 'with' clause also does not seem to apply :-)

     do
       local f, s, var = explist
       local <mark> s = s -- added!
       while true do
         ...

Am I missing something?

--
Patrick Donnelly

Reply | Threaded
Open this post in threaded view
|

Re: (not) handling new programming idioms with grace

John Belmonte
On Wed, Jul 25, 2018, 6:46 AM Patrick Donnelly <[hidden email]> wrote:


     do
       local f, s, var = explist
       local <mark> s = s -- added!
       while true do
         ...

Am I missing something?

We're not talking about iteration in general, rather iterators and specifically the case where ownership of the resource is encapsulated within the iterator.
Reply | Threaded
Open this post in threaded view
|

Re: (not) handling new programming idioms with grace

John Belmonte
Sorry didn't realize that was psuedocode.  Yes, same thing I proposed in another thread.  But exit func must be a new 4th parameter else you'll break a lot of existing code.

On Wed, Jul 25, 2018, 9:39 AM John Belmonte <[hidden email]> wrote:
On Wed, Jul 25, 2018, 6:46 AM Patrick Donnelly <[hidden email]> wrote:


     do
       local f, s, var = explist
       local <mark> s = s -- added!
       while true do
         ...

Am I missing something?

We're not talking about iteration in general, rather iterators and specifically the case where ownership of the resource is encapsulated within the iterator.
Reply | Threaded
Open this post in threaded view
|

Re: (not) handling new programming idioms with grace

Patrick Donnelly
On Tue, Jul 24, 2018 at 5:43 PM, John Belmonte <[hidden email]> wrote:
> Sorry didn't realize that was psuedocode.  Yes, same thing I proposed in
> another thread.  But exit func must be a new 4th parameter else you'll break
> a lot of existing code.

I don't really see why? IIRC, the proposal was that the __exit
metamethod is called only if it exists. So if you want your "state"
object to be released if the loop breaks, add an __exit metamethod to
new code. Fortunately, it doesn't break anything in old versions of
Lua; it maintains the status quo.

--
Patrick Donnelly

Reply | Threaded
Open this post in threaded view
|

Re: (not) handling new programming idioms with grace

Roberto Ierusalimschy
> On Tue, Jul 24, 2018 at 5:43 PM, John Belmonte <[hidden email]> wrote:
> > Sorry didn't realize that was psuedocode.  Yes, same thing I proposed in
> > another thread.  But exit func must be a new 4th parameter else you'll break
> > a lot of existing code.
>
> I don't really see why? IIRC, the proposal was that the __exit
> metamethod is called only if it exists. So if you want your "state"
> object to be released if the loop breaks, add an __exit metamethod to
> new code. Fortunately, it doesn't break anything in old versions of
> Lua; it maintains the status quo.

This is correct.

One problem with this proposal (or the similar one with a 4th parameter)
is that we will have to create the scoped variable in all loops.
Consider this code:

     do
       local f, s, var = explist
       local <mark> s = s -- added!
       while true do
         ...

The creation of a scoped variable needs to allocate memory, and therefore
it could fail without creating the scoped variable!
So, I think the correct code should be more like this:

     do
       local <mark> _s = nil -- added!
       local f, s, var = explist
       _s = s -- added!
       while true do
         ...

Anyway, Lua will have to allocate a scoped variable for every loop,
independently whether it needs it or not.

-- Roberto

12345