Source this script:
vim9script echo null == null_string # true echo null_string == '' # true echo '' == null # false echo null == null_dict # true echo null_dict == {} # true echo {} == null # false # etc…
Fundamental mathematical properties such as transitivity of equality should not be violated.
Mandatory wat!? Jokes apart, couldn't be null comparisons be always made false? Or null_string be different from an empty string, null_list be different from an empty list, etc.?
More generally, are all those different types of nulls necessary? One of the design principles of Vim 9 script is to use conventions that are typical of other modern languages. It seems to me that that principle is violated in this case.
9.0.1117
macOS 13.1
Apple Terminal
xterm-256color
ZSH 5.8.1
No response
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
Nulls are thorny, sure. My proposal is this: disallow null_<type> in comparisons—allow only in assignments and as defaults for function parameters.
This shouldn't be a dramatic change, although not backward-compatible. IMO, it would make life simpler for the programmer, which currently has to consider the subtle difference between x == null and x == null_<type>. My proposal is: use null everywhere except where a type mismatch error would be raised.
If you think that it's too late to make a change like this, I would consider at least to recommend it as a best practice in the help (unless I am missing something, which is likely).
The use cases for null_<type> that I infer from the help are essentially two:
Those are fine uses, IMO, and also cases in which null cannot be used because of the type mismatch. The example from the help file:
def F(b: blob = null_blob) if b == null_blob # b argument was not given
could become:
def F(b: blob = null_blob) if b == null # b argument was not given
AFAICS, currently those two snippets behave differently only for F(0z). But since emptiness can be checked for, one may write:
def F(b: blob = null_blob) if b == null # b argument was not given (or was null_blob) elseif empty(b) # b argument was given, but was zero-length
This allows the function to distinguish a situation in which an argument was not provided (maybe because there was no value to pass) from a situation in which an argument is provided, but it's empty:
def F(l: list<any> = null_list) if l == null echo "null" else echo printf("not null, %s empty", empty(l) ? '' : 'not') endif enddef F() # null F(null_list) # null F([]) # not null, empty F([0]) # not null, not empty
Python, for example, behaves the same with None.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
Yes, the non-transitive relationships are real, though now it has been documented explicitly in vim9.txt under *null-compare* and *null-details*. The updated documentation acknowledges this behaviour and provides guidance on how to work with it. Although it may seem counterintuitive - arguably even invalid from a mathematical view point - it exists to support practical use cases. Consider the "bonds" example, which was added to vim9.txt with 54cc820:
vim9script
var bonds: dict<list<string>> = {g: ['007', '008'], o: ['007', '009']}
def Search(query: string): list<string>
return query == "\r" ? null_list : bonds->get(query, [])
enddef
echo "Goldfinger (g) or Octopussy (o)?: "
const C: string = getcharstr()
var result: list<string> = C->Search()
if result == null # <<< DO NOT USE null_list HERE!
echo "Error: Nothing was entered"
else
echo result->empty() ? $"No matches for '{C}'" : $"{result}"
endif
The key distinction, using null in the comparison, allows differentiating between an error condition (nothing entered, returns null_list, equals null) versus a valid empty result (nothing matched, returns [], does not equal null). Using null_list in the comparison would fail because [] == null_list is true, and null cannot be returned because it would be a type mismatch.
The documentation now provides clear guidance: "For familiar null compare semantics, where an empty container is not equal to a null container, do not use null_<type> in a comparison... compare against null, not null_<type>."
As this behaviour is intentional, serves practical purposes, and is now more thoroughly documented (including a practical working example), this issue can be closed, @yegappan? The concern has been addressed by treating this as a documented feature rather than a bug.
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
TL;DR;
To me the “fix” for this issue is to discourage people from using null_<type> in comparisons: that information is already in the help and I don't think that much more explanation is needed. I wish the language enforced such a rule, but I if it's too late I can live with that.
Not doing anything with this issue is a possible resolution, if it's too late too make a backward-incompatible change. But I stand by my point of view above: disallowing null_<type> in comparisons would keep things sane. My point is that null_<type> has a totally ad-hoc comparison semantics, which doesn't add anything.
Uninitialized variables:
| Declaration | empty() |
== null |
== null_<type> |
|---|---|---|---|
| var n0: number | true | false | n.a. |
| var f0: float | true | false | n.a. |
| var b0: blob | true | false | true |
| var s0: list | true | false | true |
| var d0: dict | true | false | true |
| var t0: tuple | true | false | true |
| var F0: func | true | true | true |
| var c0: C | true | true | true |
| var s0: string | true | true | true |
| var j0: job | true | true | true |
| var ch0: channel | true | true | true |
Initialized with null_<type>:
| Declaration | empty() |
== null |
== null_<type> |
|---|---|---|---|
| var b1: blob = null_blob | true | true | true |
| var s1: list = null_list | true | true | true |
| var d1: dict = null_dict | true | true | true |
| var t1: tuple = null_tuple | true | true | true |
| var F1: func = null_function | true | true | true |
| var c1: C = null_object | true | true | true |
| var s1: string = null_string | true | true | true |
| var j1: job = null_job | true | true | true |
| var ch1: channel = null_channel | true | true | true |
Initialized with an empty value:
| Declaration | empty() |
== null |
== null_<type> |
|---|---|---|---|
| var n2: number = 0 | true | false | n.a. |
| var f2: float = 0.0 | true | false | n.a. |
| var b2: blob = 0z | true | false | true |
| var s2: list = [] | true | false | true |
| var d2: dict = {} | true | false | true |
| var t2: tuple<...list> = () | true | false | true |
| var c2 = EmptyClass.new() | true | false | false |
| var s2: string = '' | true | false | true |
| var j2: job = job_start('lss') sleep 1 |
true | false | false |
| var ch2: channel = ch_open('127.0.0.1:1234') ch_close(ch2) |
true | false | false |
Initialized with a non-empty value:
| Declaration | empty() |
== null |
== null_<type> |
|---|---|---|---|
| var n3: number = 1 | false | false | n.a. |
| var f3: float = 1.0 | false | false | n.a. |
| var b3: blob = str2blob(['x']) | false | false | false |
| var s3: list = ['x'] | false | false | false |
| var d3: dict = {x: 'x'} | false | false | false |
| var t3: tuple<...list> = (1,2) | false | false | false |
| var c3 = NonEmptyClass.new() | false | false | false |
| var s3: string = 'x' | false | false | false |
| var j3: job = job_start('sleep 1') | false | false | false |
| var ch3: channel = ch_open('127.0.0.1:1234') | false | false | false |
| var F3: func = x => () | false | false | false |
Afaict, non-null functions can never be “empty”, and non-null objects (instances of classes) may or may not depending on how they implement their empty() method.
Now, what is an intuitive semantics for null_<type>? The only rules you can extrapolate from the tables above are:
something == null is true then something == null_<type> is true.something == null_<type> is true then empty(something) is true.That's very limited. Why would I ever want to use null_<type> instead of checking for null and empty()? The latter are straightforward regardless of type:
null_<type> is empty and null;I apologize if I am missing something as quite some time has passed since the last time I looked into this
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
Well, my proposed “fix” is actually just a mitigation. The fundamental problem remains: non-null-and-empty is often equal to null-and-empty.
var s1 = null var s2 = null_string var s3 = '' echo s1 == s2 # true echo s2 == s3 # true echo s3 == s1 # false
And similarly for the other types, with the notable exception of jobs, channels and objects, afaics. This really hurts me, and reading the current documentation doesn't make me feel better, honestly.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()