Revisiting PEP 505

→ Fairly right.

The class is actually implementing boolean logic while keeping a link to an object (so it is actually a “linked bool”). It should stick to this bare minimum functionality according to the single-responsibility principle. Furthermore, creating objects with non-trivial behavior is footgun.

(Side note : any function returning a bool could be passed at init and called within __bool__() so it could extends the usage scope outside of None assessment.)

Signature should be akin to

LinkedBool(object, f=lambda x: x is not None)

and the class should have a get method.
Thus the user could use the natural boolean operators and get the selected object back with a syntax like (quick and dirty example) :

nn = LinkedBool
f_el = lambda l: l == []  # bool function for empty list assessment
selected_object = (nn(A) | nn(B) & not nn(C) ^ nn(D, f=f_el)).get()

→ now the A ?? B has become (nn(A) | nn(B)).get()

But I think allowing to perform direct operations between these objects and other types will lead to some debugging hell at some point.

No the bool operations and, or, etc. are still needed. For example, filtering a comprehension. The + method acts like ?? and returns left arg if left is not None, else right. Also and isn’t correct because + returns None or a value, not a bool. It is common in math literature to use + for or like things, etc. + is already used for many or like operations like concatenation in Python.

True : or, +, |, || should perform the same operation on boolean types (as well as and, *, &, &&). But you want to use symbols that explicitly show you are manipulating booleans in your code, it is more self-explanatory.
False : concatenation is not an or operation.

I considered for now the elements of two linked booleans should be concatenated as a result of the and operation, because basically, when you need to check for a and b, it is because you need both. Thus these two statements should be equivalent:

(nn(a) and nn(b)).get()
[a, b] if a is not None and b is not None else None

… and that’s exactly why I’d like to see a ?? operator.
There’s no context in Python where a + behaves like that, and it’s super confusing to suddenly overlay it with one that does IMHO.

Addition is not an “or”-like operation. It’s literally “this plus that”. Two plus three is five. Two characters and three characters is five letters. Two plus nn(None) should be, well, either two or an error, depending on the semantics of nn – which don’t need to be set in stone. Crucially, the same thing applies to 2 * nn(None).

2 Likes

Some tests again have shown me that trying

(nn(A) and nn(B)).get()

Will always return B, whatever I code in the __and__ method. Because the __bool__() are evaluated for each operand and then python and apllies, which return the right operand when both evaluate to True (this is a python “short-circuit”).
However, this will execute the __and__:

(nn(A) | nn(B)).get()

(This is not a problem for or because there is no difference between the builtin and the desired behavior)

There will also be a problem for the not : not nn(A) will return A is not None, losing the linked reference to A.

+ and * should be avoided, because “In the face of ambiguity, refuse the temptation to guess.”, so le’ts stick to boolean operators for boolean operations.

Therefore, to have a properly adjustable consistent behavior within an homogeneous formalism, the list of operators should be (|, &, ^, ~, @), for what can be done right now.

This is how it should have been done in the first place. But considering we already migrated once away from Optional[T] to T | None I doubt another change will be welcome.

It’s really not. Not when you have style guides that demand spacing around every |. It’s also a lot of noise when a ? conveys the meaning exactly, and is also used in other languages such as typescript and lua.

Paul, this argument falls flat on its face when you look at how the @ operator made it in. It doesn’t universalize.

1 Like

Notably, this list does not include a shortcut operation (like || / or).

|| raises syntax error in python, | is the equivalent. shortcut means the behavior of and and or cannot be overridden (but it is not a problem for or)

Also erratum : there is no logic behind a not operator for a linked bool, so ~ does not stand.
Also update : I missed the magic method __getitem__, and it works, so @ is not necessary.
Also novelty : there might also be a possibility to just filter the Nones out of the given arguments, but we need a proper operator for this, let’s consider >> for example :

(nn(A) >> nn(None) >> nn(B)).get()  # returns [A, B]

Also consideration : If in the future we get a possibility to do, for example, ?{ A | B }, then the overridden operators must act as a separators, and we want to keep the regularly used operators like +, *, etc… another reason not to override them.

In the NL code above some people have objected to using operators + (or like), * (and like), @ (xor like), >> (implication like) and have suggested and and or.

There are not enough boolean operators, only 2, and they are already used for normal boolean operations in NL, e.g. [(x, y) for x in xs for y in ys if NL(x) and NL(y)]=[(0, 1), (0, 2), (1, 1), (1, 2)] for xs = [0, 1, None] and ys = [None, 1, 2] (note treatment of 0 as true in NL).

Would people prefer these operators |, &, ^, and >>?

There is something else, another way.
By using propagation of the nn containers without the operators, and by defining an “initialiser-closer” element, for example ñ, and using <<, >> as opener and closer. We can actually use this form :

selected = ñ<< A | B >>ñ
# selected = (nn(A) | nn(B)).get()

This is a “hacky” solution, we are ‘kind of’ emulating an evaluation within a space where the bitwise operators are overloaded (this is not exactly correctly expressed, I might provide better explanation, or prototype code, later). This would not have proper syntax checks though, so it should be properly handled/closed, else you are going to instanciate objects with counter-intuitive and propagative properties, and I decline any responsibility for brain damage if you do so.

This would be quite similar to:

[ñ<< x & y >>ñ for x in [0, 1, None] for y in [None, 1, 2]]
# [None, [0,1], [0,2], None, [1,1], [1,2], None, None, None]

You don’t want to have any + that do not operate as + conventionally does. Actually you should keep the possibility to use non bitwise operators within the chain (even if adding None to something is nonsense, you might use different boolean condition for your operands).
Also note that the precedence order of the operators would prevent you to use the algebraic operators for property propagation. or and and will also stop the property propagation by short-circuiting.

I totally agree with @methane that it should emulate JS as much as possible.

However, at the same time, to me it seems that combination of null and undefined in JS cover much larger scope than None does in Python.

So if I was to push this forward, the first question to answer would be: “Is it worth doing this for is not None alone or should it be more flexible and capture various conditions for this to be justified?”

3 Likes

I made a LinkedBool class (using the nn(...).get() syntax, along with some tests, here :

The function for boolean return is lambda x: x is not None by default but can be overridden, yet I did not experiment with it.

Is it only me that finds OT here detailed explaining and code example in dozen of posts about alternatives to PEP 505?

(assuming this means off-topic)

Right : I’ve been experimenting and ended up with LinkedBool logics development.
Yet : This logics allow for a more powerful possibility than original proposal.

I do a final proposal just next and I should be done with this topic if it does not convince people.

Here are the syntaxic replacements I propose :

a ?? b      ->      ?{ a | b }
a?[b?]      ->      ?{ a[b] }
a?.b?       ->      ?{ a.b }

So there is only one operator added into the language (which encases the linked boolean logics).
It also comes with more functionalities :

# mutually exclusive existence index
0 if (a is not None and b is None and c is None) else 1 if (a is None and b is not None and c is None) else 2 if (a is None and b is None and c is not None) else None
->     ?{ a ^ b ^ c}.xidx
# simultaneous existence
(a,b) if (a is not None and b is not None) else None
->     ?{ a & b }

Also, some list filtering might be convenient :

[x for x in xs if x is not None]
->     ?{xs}.filter()

And finally, it is extendable to cases where we need something different than is not None as the boolean assessment function for the linked bool.

# some complex thing, where we consider empty lists "falsy"
fun = lambda x: x not in [None, []]
?(fun){ (a | b[c].d) & e ^ f }

I think this thread has run its course. The proposal in PEP 505 is not reaching consensus. Alternative ideas and code experiments should be in a different threads under python-ideas, not PEPs.

17 Likes

I do think that this thread has run its course and should be closed, but don’t think alternatives are off topic.

If a PEP is failing to go forward, surely alternatives are far more valuable than simply stating what the PEP proposed. We know what is in the PEP anyway and can always look at it to refresh the memory.

Just presenting the same thing again and again seems the unproductive route to me.

2 Likes

This discussion doesn’t seem to have resolved to a consensus, hence I have posted an alternative under ideas None-safe traversal of dictionaries, e.g. from JSON.

1 Like

Shortest, least buggy polyfill I’ve seen so far @guido

def get(f, default=''):
	try: return f()
	except: return default

Usage:

get(lambda: foo['i']['may']['not']['exist'])
2 Likes

Don’t ever use a bare except. At the very least use except Exception, and even then it’s IMO too aggressive. (Although I agree that there isn’t an obviously better set of classes to catch)

1 Like