On Fri, Sep 18, 2015 at 05:49:36PM -0700, Andrew Barnert via Python-ideas wrote:
Obviously "spam?" returns something with a __getattr__ method that just passes through to spam.__getattr__, except that on NoneType it returns something with a __getattr__ that always returns None. That solves the eggs case.
Ah, and now my enthusiasm for the whole idea is gone...
In my previous response, I imagined spam?.attr to be syntactic sugar for `None if spam is None else spam.attr`. But having ? be an ordinary operator that returns a special Null object feels too "Design Pattern-y" to me. I think the Null design pattern is actually harmful, and I would not like to see this proposal implemented this way.
(In another email, Andrew called the special object something like NoneMaybe or NoneQuestion, I forget which. I'm going to call the object Null, since that's less typing.)
The Null object pattern sounds like a great idea at first, but I find it to be a code smell at best and outright harmful at worst. If you are passing around an object which is conceptually None, but unlike None reliably does nothing without raising an exception no matter what you do with it, that suggests to me that something about your code is not right.
If your functions already accept None, then you should just use None. If they don't accept None, then why are you trying to smuggle None into them using a quasi-None that unconditionally hides errors?
Here are some problems with the Null pattern as I see it:
(1) Suppose that spam? returns a special Null object, and Null.attr itself returns Null. (As do Null[item] and Null(arg), of course.) This matches the classic Null object design pattern, and gives us chaining for free:
value = obj?.spam.eggs.cheese
But now `value` is Null, which may not be what we expect and may in fact be a problem if we're expecting it to be "an actual value, or None" rather than our quasi-None Null object.
Because `value` is now a Null, every time we pass it to a function, we risk getting new Nulls in places that shouldn't get them. If a function isn't expecting None, we should get an exception, but Null is designed to not raise exceptions no matter what you do with it. So we risk contaminating our data with Nulls in unexpected places.
Eventually, of course, there comes a time where we need to deal with the actual value. With the Null pattern in place, we have to deal with two special cases, not one:
# I assume Null is a singleton, otherwise use isinstance if filename is not None and filename is not Null: os.unlink(filename)
A small nuisance, to be sure, but part of the reason why I really don't think much of the Null object pattern. It sounds good on paper, but I think it's actually more dangerous and inconvenient than the problem it tries to solve.
(2) We can avoid the worst of the Null design (anti-)pattern by having Null.attr return None instead of Null. Unfortunately, that means we've lost automatic chaining. If you have an object that might be None, we have to explicitly use the ? operator after each lookup except the last:
value = obj?.spam?.eggs?.cheese
which is (a) messy, (b) potentially inefficient, and (c) potentially hides subtle bugs.
Here is a scenario where it hides bugs. Suppose obj may be None, but if it is not, then obj.spam *must* be a object with an eggs attribute. If obj.spam is None, that's a bug that needs fixing. Suppose we start off by writing the obvious thing:
but that fails because obj=None raises an exception:
obj? returns Null Null.spam returns None None.eggs raises
So to protect against that, we might write:
but that protects against too much, and hides the fact that obj.spam exists but is None.
As far as I am concerned, any use of a Null object has serious downsides. If people want to explicitly use it in their own code, well, good luck with that. I don't think Python should be making it a built-in.
I think the first case, the classic Null design pattern, is actually *better* because the downsides are anything but subtle, and people will soon learn not to touch it with a 10ft pole *wink*, while the second case, the "Null.attr gives None" case, is actually worse because it isn't *obviously* wrong and can subtly hide bugs.
How does my earlier idea of ? as syntactic sugar compare with those?
In that case, there is no special Null object, there's only None. So we avoid the risk of Null infection, and avoid needing to check specially for Null. It also avoids the bug-hiding scenario:
is equivalent to:
None if obj is None else obj.spam.eggs
If obj is None, we get None, as we expect. If it is not None, we get obj.spam.eggs as we expect. If obj.spam is wrongly None, then we get an exception, as we should.