[vim/vim] [vim9] Equality is not transitive (when nulls are involved) (Issue #11770)

11 views
Skip to first unread message

Lifepillar

unread,
Jan 1, 2023, 1:01:25 PM1/1/23
to vim/vim, Subscribed

Steps to reproduce

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…

Expected behaviour

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.

Version of Vim

9.0.1117

Environment

macOS 13.1
Apple Terminal
xterm-256color
ZSH 5.8.1

Logs and stack traces

No response


Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/issues/11770@github.com>

Bram Moolenaar

unread,
Jan 1, 2023, 1:32:23 PM1/1/23
to vim/vim, Subscribed


> ### Steps to reproduce
>
> Source this script:
>
> ```vim

> 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…
> ```
>
> ### Expected behaviour

>
> Fundamental mathematical properties such as transitivity of equality
> should not be violated.

Well, would be nice, but this is a script language, mathematics is just
one of the things we might want to take into account.

> Mandatory [wat!?](https://www.destroyallsoftware.com/talks/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.?

An empty string and a null string are considered to have the same value.
Same for list and dict. This is mainly so that uninitialized variables
are like "empty" variables.

If you care about the difference use the "is" operator.


> 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.

Yes, they were added for a reason.

Changing this now most likely causes more trouble than it would solve.
If we could agree on how it should work anyway. Before the Vim 9
release we might have considered changes, but now that users rely on the
current behavior any changes are going to create obscure problems.

--
"I love deadlines. I especially like the whooshing sound they
make as they go flying by."
-- Douglas Adams

/// Bram Moolenaar -- ***@***.*** -- http://www.Moolenaar.net \\\
/// \\\
\\\ sponsor Vim, vote for features -- http://www.Vim.org/sponsor/ ///
\\\ help me help AIDS victims -- http://ICCF-Holland.org ///


Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.Message ID: <vim/vim/issues/11770/1368504864@github.com>

Lifepillar

unread,
Jan 2, 2023, 7:45:27 AM1/2/23
to vim/vim, Subscribed

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:

  • to clear a variable;
  • as a default for a parameter.

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.Message ID: <vim/vim/issues/11770/1368916600@github.com>

Peter Kenny

unread,
Jan 24, 2026, 6:36:04 PM (17 hours ago) Jan 24
to vim/vim, Subscribed
kennypete left a comment (vim/vim#11770)

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.Message ID: <vim/vim/issues/11770/3795699205@github.com>

Lifepillar

unread,
9:00 AM (3 hours ago) 9:00 AM
to vim/vim, Subscribed
lifepillar left a comment (vim/vim#11770)

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:

  1. if something == null is true then something == null_<type> is true.
  2. ifsomething == 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:

  • something initialized with null_<type> is empty and null;
  • something initizialized with an empty value is empty and non-null;
  • something initialized with a non-empty value is non-empty and non-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.Message ID: <vim/vim/issues/11770/3796703593@github.com>

Lifepillar

unread,
10:45 AM (1 hour ago) 10:45 AM
to vim/vim, Subscribed
lifepillar left a comment (vim/vim#11770)

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.Message ID: <vim/vim/issues/11770/3796852077@github.com>

Reply all
Reply to author
Forward
0 new messages