dao icon indicating copy to clipboard operation
dao copied to clipboard

Possible to define uncallable routines

Open ShadauxCat opened this issue 10 years ago • 64 comments

Playing around seeing if I could figure out whether or not Dao has destructors, I made a class that looks like this:

class MyClass
{
    routine MyClass()
    {
        io.writeln("Construct");
    }

    routine ~MyClass()
    {
        io.writeln("Destruct");
    }
}

This works completely fine! Obviously since this does not seem to be the way to define destructors, it doesn't seem to do anything, but it doesn't throw an error.

Until I do this:

var mc : MyClass = MyClass();
mc.~MyClass();

Then it throws a fit.

This is really kind of a minor issue, but as a polish item, it seems like this is something the compiler should throw an error about at the time an invalid method is defined rather than when it's called.

ShadauxCat avatar Feb 23 '15 03:02 ShadauxCat

BTW, destructors are not supported for Dao classes, they may only be defined for wrapped types defined in C.

Night-walker avatar Feb 23 '15 08:02 Night-walker

That answers one question I had then. Any chance they could be added? Destructors prevent programmer error, and I think a language based on a philosophy with key tenets of "naturalness" and "action without effort" should make it easy for developers to avoid errors.

I can provide plenty of examples of cases where destructors prevent errors. First one that comes to mind is a logging class that opens a file handle. If the programmer forgets to call "close" on it and then the reference to the logger goes out of scope, a file handle has been leaked. Do that enough times, you run out of handles and crash. Destructors keep the programmer from having to worry about that because if they forget to call close explicitly, the destructor will clean it up.

(Key point being that memory is not the only resource a class can allocate. They can take file handles, acquire semaphores, open sockets, or even consume software resources, like for example in a game, occupy space in a grid. I think classes need a reliable way to clean that up. I can see them being used much less than in C++, but they're still important. I can't stand working with OOP in JavaScript and the lack of destructors is one reason why.)

ShadauxCat avatar Feb 23 '15 12:02 ShadauxCat

There is a whole bunch of questions arising from existence of destructors. Especially the time when they're called has to be precisely specified, but more importantly managed by the programmer. This is very difficult and that's why we use defer(){} instead for all this non-pure stuff. It forces the programmer to strictly structure the program, so the leaks are prevented (but still possible).

dumblob avatar Feb 23 '15 12:02 dumblob

I agree that if the programmers care about when the cleanup happens, then it's reasonable to require them to use something like defer(){}. But there are lots of cases where it doesn't matter when resources are cleaned up, only that they are. I'd say cleanup happening at an unpredictable time is better than not at all, personally.

ShadauxCat avatar Feb 23 '15 12:02 ShadauxCat

I'll use C# as an example. In C# objects are destroyed whenever the garbage collector happens to run. There's no way to predict it. But C# still has destructors and they're still very useful even though the timing can't be predicted.

ShadauxCat avatar Feb 23 '15 12:02 ShadauxCat

But there are lots of cases where it doesn't matter when resources are cleaned up, only that they are.

Then it shouldn't be an issue to clean them ASAP, which is a perfect fit for defer(){}.

dumblob avatar Feb 23 '15 12:02 dumblob

Problem with using defer(){} in that situation is that it can lead to a lot of repeated code.

In the logger case, consider the following two examples, assuming a Logger class that opens a file on construction:

Example 1:

routine Routine1()
{
    l = Logger("Routine1.log");
    # Do some stuff.
}
routine Routine2()
{
    l = Logger("Routine2.log");
    # Do some stuff.
}
routine Routine3()
{
    l = Logger("Routine3.log");
    # Do some stuff.
}

Example 2:

routine Routine1()
{
    l = Logger("Routine1.log");
    defer()
    {
        l.Close();
    }
    # Do some stuff.
}
routine Routine2()
{
    l = Logger("Routine2.log");
    defer()
    {
        l.Close();
    }
    # Do some stuff.
}
routine Routine3()
{
    l = Logger("Routine3.log");
    defer()
    {
        l.Close();
    }
    # Do some stuff.
}

The first example uses destructors. The second uses defer(){}. defer(){} is a GREAT concept and I love it for situations where I need to do some specific handling only once or a few times, but without destructors, requiring its use is pushing the burden of cleaning up resources on the client software, and leading to potentially a lot of copy-pasted code that could become a maintenance issue. With destructors, the burden of cleanup is placed on the module developer, cleanup only has to be maintained in one place, and the client code ends up being both simpler and cleaner.

ShadauxCat avatar Feb 23 '15 13:02 ShadauxCat

As an even more complicated example:

Suppose I write a class that contains a Logger as a member. The logger is private and no one knows it's there. My class has nothing to do with logging, it just logs debugging output, so the person working with it isn't acutely aware of the fact that my class even has a Close() function on it. In this case, it's highly probable that a programmer won't even think about the need for a defer(){} block for this class and will just use it and let it go out of scope, and won't notice that they're leaking file descriptors until their code goes live and crashes after running for three weeks. Then they have to go back and add defer() blocks in every place where they use my class, or close it manually in cases where defer() blocks won't work because it's a resource shared by multiple functions. Maybe THEY'VE even created a class that contains MY class so now they have to add a Close() method to their class to clean up my class so my class can clean up the logger class.

It can keep getting more and more complicated and add up to days of work to fix the problem.

If Logger has a destructor that's called automatically when it's cleaned up, none of these other classes even have to think about it. It happens automatically when their class goes out of scope and logger no longer has any references to it.

ShadauxCat avatar Feb 23 '15 13:02 ShadauxCat

Why don't you want to use (class) decorators for such repetitive patterns?

dumblob avatar Feb 23 '15 13:02 dumblob

I'm not certain how a class decorator would solve the problem. If it forces a defer() block that destructs the class, that would break cases where the class needs to stick around (i.e., member of another class). If it's a function decorator, that still puts the burden of remembering to use that decorator on the client code.

Let me counter with this: Why do you want to not have destructors? They exist in almost every object-oriented language I've ever worked with, from the simple single-threaded (python) to the multi-threaded (C#) to the potentially massively multi-threaded (C++). Javascript doesn't count because it's only barely object oriented - OOP in Javascript is like hammering in a nail with the handle of a screwdriver.

I understand that I've come in and opened several tasks within a short period of time and maybe that makes you feel like I'm asking for the moon. But I think I've also shown I'm willing to do the work. When I open issues, more than anything, I'm just looking for approval of the idea; once it's approved I'm willing to write it myself. So I'm not trying to drop a ton of work on your shoulders.

But I do keep getting a lot of responses from both of you to the tune of "I haven't needed it, so it must not be necessary."

Just to give a little background on who I am and why I'm asking for these things that may seem insignificant for day-to-day use - I'm a AAA game developer. I've worked on very large codebases (Mass Effect, Star Wars: The Old Republic). I've seen projects get HUGE.

I've seen how small variances in memory layout (like using a map to bool instead of a set) can ruin cache coherency and destroy performance. I've seen how those extra bools can cause a game to blow out the limited memory available on console platforms, or cause serious memory fragmentation that results in being unable to allocate.

I've seen maintainability issues like not having destructors or finalizers completely destroy a project.

I'm not trying to be contrary or hostile here (and I really hope this isn't taken that way), but like I said, I'm getting a lot of responses from both you and @Night-walker saying that things are not necessary because you've never needed them. But as I've said before... just because you don't need it doesn't mean no one does. If I didn't need it - if I hadn't needed and used it countless times in the past - I wouldn't ask for it.

Believe me when I say that I'm not trying to spit all over Dao. Truth is, I love Dao. I want to use it in a game project I'm working on. I want to use it as a complete replacement for python for my personal use. But some of these issues I've filed will prevent me from doing either. I'm filing these issues because I want Dao to succeed and I want Dao to be a language I can take to other game developers on future projects and say "hey, we should check this language out." But I know without some features (like destructors), using Dao for a huge project like a game will be a non-starter for them.

I really am trying to help. I promise. Both by providing feedback from my experiences using the language, and by actually contributing code. I apologize if I've come on too fast and too strong, but I really do want to help, not hurt.

ShadauxCat avatar Feb 23 '15 13:02 ShadauxCat

Point of semantics; I'm using "destructor" and "finalizer" interchangeably here when they are actually different things in some ways. Destructors are preferable to finalizers for a number of reasons but finalizers are still better than nothing.

ShadauxCat avatar Feb 23 '15 14:02 ShadauxCat

But I do keep getting a lot of responses from both of you to the tune of "I haven't needed it, so it must not be necessary."

A simple principle: deem any feature useless unless the opposite is proved. When you say "It's good, I use it, other folks use it", it's not sufficient. So I counter it with "And I don't think so", prompting you to come up with better arguments. If you propose something, be ready to back your idea with solid reasoning -- it's often more important then the implementation itself.

If you feel we have inert attitude toward your idea, you aren't trying good enough to make us interested :)

Night-walker avatar Feb 23 '15 15:02 Night-walker

Just to give a little background on who I am and why I'm asking for these things that may seem insignificant for day-to-day use - I'm a AAA game developer. I've worked on very large codebases (Mass Effect, Star Wars: The Old Republic). I've seen projects get HUGE.

Well, now I am interested. I play those AAA games :) That's it, you have found my soft spot :)

Night-walker avatar Feb 23 '15 15:02 Night-walker

Ok, back to the point. Destructors. So far, there was no use for them in classes -- all the resources like file descriptors are acquired and released in wrapped types which actually do have destructors. As @dumblob pointed out, destructors also lead to certain complications in the control flow which can lead to various unpleasant effects.

Again, it doesn't mean I consider destructors useless. But they require additional considerations worth a dedicated issue, with proper evaluation of all pros and cons, as well as alternatives.

Night-walker avatar Feb 23 '15 16:02 Night-walker

A simple principle: deem any feature useless unless the opposite is proved. When you say "It's good, I use it, other folks use it", it's not sufficient. So I counter it with "And I don't think so", prompting you to come up with better arguments. If you propose something, be ready to back your idea with solid reasoning -- it's often more important then the implementation itself.

If you feel we have inert attitude toward your idea, you aren't trying good enough to make us interested :)

I can understand that principle in general. That's why I suggested #381 - I felt I wasn't doing a good job of presenting my feature requests and that a more rigid proposal format would improve that for myself and others.

Ok, back to the point. Destructors. So far, there was no use for them in classes -- all the resources like file descriptors are acquired and released in wrapped types which actually do have destructors

By wrapped types, I assume you mean exposed C types.

As @dumblob pointed out, destructors also lead to certain complications in the control flow which can lead to various unpleasant effects.

Again, it doesn't mean I consider destructors useless. But they require additional considerations worth a dedicated issue, with proper evaluation of all pros and cons, as well as alternatives.

Sure, that makes sense, I'll open a separate issue later. I honestly don't expect destructors to be used much in Dao - they're generally not needed in garbage collected languages. But when they're needed, they're usually really needed.

ShadauxCat avatar Feb 23 '15 16:02 ShadauxCat

By wrapped types, I assume you mean exposed C types.

I mean non-core types which get into the language via DaoNamespace_WrapType(), like io::Stream or mt::Future.

I honestly don't expect destructors to be used much in Dao - they're generally not needed in garbage collected languages. But when they're needed, they're usually really needed.

I agree that such need may eventually arise. Some tool to address such cases may be needed, be it destructor or something a bit different (e.g., as in Ruby).

Night-walker avatar Feb 23 '15 18:02 Night-walker

I've not used Ruby enough to say much about it, but a brief google search indicates that it basically has the same thing, just with a different interface. Don't like their interface much, but that's subjective, and I can see the merit of being able to define that finalizer outside the object if you want to tie its destruction to some other behavior.

ShadauxCat avatar Feb 23 '15 18:02 ShadauxCat

A note for @ShadauxCat, to clarify things. Me and @dumblob do not make decisions regarding the language design. Only @daokoder, as the author of the language, bears responsibility for determining the path Dao takes. So if you have feeling that instead of doing some work on your proposal we just mumble a few comments and subside, it's only because we're not in charge to do that.

When it comes to the standard library, you can turn to me directly regarding the modules I wrote myself or actively contributed to.

Night-walker avatar Feb 23 '15 19:02 Night-walker

All things said. @Night-walker precisely described our perception and the situation. Now we have to wait.

dumblob avatar Feb 23 '15 20:02 dumblob

A remark about destructors. There is one dangerous case to take into attention in the context of GC languages: when destructor may contain something like global_var = self.

Night-walker avatar Feb 24 '15 18:02 Night-walker

How Go solves this: SetFinalizer().

Night-walker avatar Feb 24 '15 19:02 Night-walker

Go and Ruby seem to take the same approach. The way python does it starting with 3.4 (just as another point to look at) is that you define a method named __del__() and that gets called once and only once on an object; if a reference is re-obtained during the finalizer, the finalizer won't get called again later. (Full spec: https://www.python.org/dev/peps/pep-0442/ )

I like the method Go and Ruby take better, personally. It allows for objects that are revived during the finalizer to also set a new finalizer if desired.

ShadauxCat avatar Feb 24 '15 19:02 ShadauxCat

I like the method Go and Ruby take better, personally. It allows for objects that are revived during the finalizer to also set a new finalizer if desired.

I also find it promising. It allows to dynamically add an ad-hoc finalizer to any object.

Night-walker avatar Feb 24 '15 19:02 Night-walker

It allows to dynamically add an ad-hoc finalizer to any object.

That's also a nice bonus.

What happens if the object already has a finalizer and someone adds an ad-hoc one? Does it replace the existing one or call both?

ShadauxCat avatar Feb 24 '15 19:02 ShadauxCat

What happens if the object already has a finalizer and someone adds an ad-hoc one? Does it replace the existing one or call both?

It replaces the former finalizer.

Night-walker avatar Feb 24 '15 20:02 Night-walker

Seems dangerous to add them ad-hoc, then. Seems there should also be a GetFinalizer() so you can call any existing ones as well if you're adding them ad-hoc.

How does it work with inheritance? If my base class has a finalizer and the inherited class also needs one, does the inherited finalizer just need to manually call the base one since it's getting blown away?

ShadauxCat avatar Feb 24 '15 20:02 ShadauxCat

Seems dangerous to add them ad-hoc, then.

Making some code run implicitly at an non-determined point of time is dangerous by itself. But static finalizers would be more predictable, of course.

How does it work with inheritance? If my base class has a finalizer and the inherited class also needs one, does the inherited finalizer just need to manually call the base one since it's getting blown away?

It has no connection to inheritance, and not only because Go actually doesn't support inheritance :) Finalizers work on abstract references examined by the GC. They don't mess with language syntax in general and OOP in particular, which makes them more attractive than classic destructors.

Night-walker avatar Feb 24 '15 21:02 Night-walker

This is the scenario where I see inheritance mattering:

class A
{
    routine A()
    {
        # ... Acquire some resources ...
       SetFinalizer(self, Finalizer);
    }

    routine Finalizer()
    {
        # ... free up the resources ...
    }
}

class B : A
{
    routine B() : A()
    {
        # ... Acquire some different resources ...
       SetFinalizer(self, Finalizer); # Uh-oh! A can't finalize now!
    }

    routine Finalizer()
    {
        # ... free up the different resources ...
    }
}

In this situation, constructing an object of type B results in only B's resources being freed, and A's resources end up leaking. My suggested solution:

class B : A
{
    routine B() : A()
    {
        # ... Acquire some different resources ...
       parentFinalizer = GetFinalizer(self);
       SetFinalizer(self, Finalizer); # Uh-oh! A can't finalize now!
    }

    routine Finalizer()
    {
        # ... free up the different resources ...
        if(parentFinalizer)
        {
            parentFinalizer();
        }
    }

    var parentFinalizer : routine;
}

Destructors implemented in the manner of C++/Python/C#/Java don't have this problem since they always call all the destructors in a tree, but they don't have the benefits of the nice handling of revived objects and the ability to set them ad-hoc. Go doesn't need to worry about this since they don't have inheritance, but since Dao does, it seems worth at least considering.

ShadauxCat avatar Feb 24 '15 22:02 ShadauxCat

Destructors implemented in the manner of C++/Python/C#/Java don't have this problem since they always call all the destructors in a tree, but they don't have the benefits of the nice handling of revived objects and the ability to set them ad-hoc. Go doesn't need to worry about this since they don't have inheritance, but since Dao does, it seems worth at least considering.

It doesn't matter much, actually. Go/Ruby-styled solution can achieve the same kind of behavior by stacking finalizers.

Night-walker avatar Feb 25 '15 07:02 Night-walker

Another problem with any kind of finalizers is that they need to be executed in a separate, dedicated thread. Not the thread in which the GC is running, as they can possibly freeze it.

Night-walker avatar Feb 25 '15 08:02 Night-walker