Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Let's figure out how we want && and || to work #7479

Open
rgwood opened this issue Dec 14, 2022 · 11 comments
Open

Let's figure out how we want && and || to work #7479

rgwood opened this issue Dec 14, 2022 · 11 comments
Labels
error-handling How errors in externals/nu code are caught or handled programmatically (see also unhelpful-error) semantics Places where we should define/clarify nushell's semantics

Comments

@rgwood
Copy link
Contributor

rgwood commented Dec 14, 2022

As discussed in this week's meeting, we should figure out the plan for error handling with && and ||. JT landed an initial PR recently but had to revert it.

As a starting point, let's agree on whether various data types should evaluate to success or failure for the purposes of && and ||. To recap:

foo && bar will run bar only if foo succeeded.
foo || bar will run bar only if foo failed.

Here's my (probably wrong/incomplete) first stab at the question:

Data type Success / Failure Notes
Value::Error failure
Value::Nothing success
PipelineData::Empty success
Value::Bool(false) failure this will help avoid surprises for *nix people accustomed to false(1). not 100% sure about this on
Value::Bool(true) success
List or ListStream that contains a Value::Error (in any position of the list) failure Not 100% sure about this one. ListStream gets interesting because you can't know up front whether it contains an error
Other Lists and ListStreams success
Other Value variants success
External command that returns a 0 exit code success
External command that returns a nonzero exit code failure This should handle 90% of use cases. If people need different exit code behaviour they can use do -i or something similar

@jntrnr I think this is what you were asking for, just let me know if I missed something.

@webbedspace
Copy link
Contributor

Wait, null has to be falsy, right?

Also, I wonder if this falsiness table should be used with any and all… Currently those have rather dubious behaviour when their closures produce non-booleans…

@rgwood
Copy link
Contributor Author

rgwood commented Dec 16, 2022

Good call, I think null/nothing should be falsey.

Since #7383 we have a way to distinguish between an empty pipeline and a null value, which I think is essential.

edit: I'm actually less sure about this after further thought. I could go either way on null/nothing.

@Silentdoer
Copy link

Why not keep it consistent with mainstream languages? For example, in JavaScript, empty string and 0 are false

@rgwood
Copy link
Contributor Author

rgwood commented Dec 18, 2022

I don't think we want to treat "" and 0 as false. As I understand it that's generally regarded as a design mistake in JS.

@webbedspace
Copy link
Contributor

It's not really a mistake per se (Python also does this, albeit with a simple, robust rule about what values are and aren't falsy) but it's not necessarily desirable for Nushell.

By the way… I think PipelineData::Empty should be falsy as well, for consistency with null (a lot of the time, the two "values" are considered identical, e.g. with describe).

@rgwood
Copy link
Contributor Author

rgwood commented Dec 18, 2022

By the way… I think PipelineData::Empty should be falsy as well, for consistency with null (a lot of the time, the two "values" are considered identical, e.g. with describe).

My thinking is that many commands return PipelineData::Empty on success. For example, we want print "foo" && print "bar" to execute both print commands.

It's possible that I'm using the terms "truthy" and "falsy" in a slightly misleading way here, perhaps "success" and "failure" would be more accurate.

@webbedspace
Copy link
Contributor

Hmm, good point. "Success" and "failure" are a bit more enlightening.

@theAkito
Copy link

It's really tough to fit the original purpose of this traditional feature into Nu's internal commands.

External command that returns a nonzero exit code failure

This is the most important & significant advantage of this feature.
The whole || & && idea is based on the fact, that all traditional commands have some sort of exit code.
It is simply impossible to have a 1:1 equivalent of this behaviour connected to internal Nu commands.

For example, nothing should probably lead to success, as so many commands succeed and output nothing. However, if that weren't the case, maybe it should lead to failure. It depends.

Same with Lists, etc. It wasn't even discussed as it seemed obviously "successful", however does it really mean that? If I get a List of erroneous stuff (erroneous, without having an actual Nu error, I guess), then it's still kind of a "failure". However, it cannot be detected by Nu (except it contains an actual Nu error), which leads to my initial point, that it's impossible to have a 1:1 equivalent of this behaviour in Nu.

Due to these reasons, I recommend one of two solutions for the time being.

  1. Either make this behaviour only available for external commands & not for internal Nu commands. This would support a 1:1 clone of the original behaviour, everyone knows. Nobody is used to this behaviour in Nu, as it was never there before, so this is the safest & most compatible method. If this feature only supports external commands, every single user out there will get it & not raise any eyebrows, as it is literally a 100% replicating traditional shell behaviour.

  2. If it is decided to include internal Nu commands in this feature, then make it as loose as possible. Only mark those return values as failures, which are clearly understandable as errors by every single user. It ought not to be in any way ambiguous. That means, that the only two return values everyone can agree on being "failures" in the traditional sense are "wrong" exit codes from external commands, Nu Errors as return values & Boolean false. All other return values are up on the debate table & it will mostly be a matter of preference. It would also be important that it fits the current & future proposed Nu philosophy & its resulting guidelines. You don't want to change crucial stuff like this constantly.

As far as your current table goes, I think that's acceptable & understandable. Anything with a Nu Error is a "failure". Boolean false is a "failure", as well.

That said, I would very very very very very strongly recommend against taking examples from languages like JavaScript & Python. For example, JavaScript is basically the incarnation of a design failure. This language does not contain design failures. It is a design failure in itself. It shouldn't even exist, not to mention be popular.
JavaScript is the C language for browsers. It has spread everywhere, just like C(ancer).

@sholderbach sholderbach added semantics Places where we should define/clarify nushell's semantics error-handling How errors in externals/nu code are caught or handled programmatically (see also unhelpful-error) labels Dec 20, 2022
@webbedspace
Copy link
Contributor

webbedspace commented Dec 20, 2022

As someone who's worked on a single JS codebase for over 8 years, I'll say that TypeScript is pretty fire emoji though, which kind of says a lot about how much of JS's problems boil down to "the type system".

Also, funny coincidence, but I just came back from reading a tweet about how a friend of a friend of a friend just succumbed to cancer, into reading this post, so…… a bit unsmooth, my fellow.

…Anyway, back on topic:
I think something that's being forgotten here is that most Nushell commands are side-effect-free. That means the whole concept of "failure" is kind of immaterial for them. This simplifies the process of dealing with null/nothing considerably:

For example, nothing should probably lead to success, as so many commands succeed and output nothing. However, if that weren't the case, maybe it should lead to failure. It depends.

Rather than "it depends", only side-effectual commands (mv, cp etc.) that output nothing should be considered to be successful or failing. However, when you dig down into them, you notice something: they actually output PipelineData::Empty, not Value::Nothing, which is an interesting distinction. PipelineData::Empty is an "implicit null" (to use JS parlance, though there it would be an implicit undefined) whereas Value::Nothing is an "explicit null", null-as-data. Hence, we can simply say that Value::Nothing is equivalent to a boolean false, whereas PipelineData::Empty (or any PipelineData) is equivalent to success.

This also fits in well with the PR for nullable cell paths #7540: for $a.b?.c && rm $a.b.c, $a.b?.c would produce an explicit null if $a.b?.c does not exist, and && would only run the rm if $a.b?.c exists.

@theAkito
Copy link

I'll say that TypeScript is pretty fire emoji though, which kind of says a lot about how much of JS's problems boil down to "the type system".

As much as I am against Micro$oft, I have to agree, that TypeScript is the emergency chute for JavaScript. It handles so many JavaScript related problems. Yes, mainly the type system.

Also, funny coincidence, but I just came back from reading a tweet about how a friend of a friend of a friend just succumbed to cancer, into reading this post, so…… a bit unsmooth, my fellow.

Sorry about that. I just see the language C as a serious sickness in the general programming world & JavaScript is basically the same, but for browsers.
As the analogy is so fitting, I am using it to emphasise my point about it. I don't know any other method of emphasis, that makes the emphasis so drastic & significant as I want to portray it as. Any other method I came up with wasn't as drastic, which is why I seem to have no choice but to express this analogy.

However, once everyone stops using C for everything but embedded & legacy projects, I won't need an analogy as drastic, because the sickness would be cured.
People need to stop using C & start using Rust.

I will already be satisifed, once Linux is mostly (re-)written in Rust. Once it crosses the 50% line, C will become insignificant enough.

Rather than "it depends", only side-effectual commands (mv, cp etc.) that output nothing should be considered to be successful or failing. However, when you dig down into them, you notice something: they actually output PipelineData::Empty, not Value::Nothing, which is an interesting distinction.

Okay, I never dove this deep into it, but the first question that is raised in my head, when reading this is, whether this is a rule or more or less a coincidence. I mean, if we expect the behaviour the way you describe, then it must be followed in all future implementations of similar commands or whatever comes next in Nu. Basically, it has to become a rule or guideline a developer has to actively follow.

PipelineData::Empty is an "implicit null" (to use JS parlance, though there it would be an implicit undefined) whereas Value::Nothing is an "explicit null", null-as-data.

Now that is truly interesting. Thank you very much for the insight. I didn't think about it, that way. Nice!

Hence, we can simply say that Value::Nothing is equivalent to a boolean false, whereas PipelineData::Empty (or any PipelineData) is equivalent to success.

Ironically, when reading & processing the preceding sentence, my brain came to the exactly opposite conclusion.
As in....

  • I know what I'm doing! Here is null! Do what you want with it. I know what I am doing. (Success)
  • I don't know what I'm doing! Here is my empty result. I don't know what I'm doing, so I am giving an empty result. (Failure)

This also fits in well with the PR for nullable cell paths #7540: for $a.b?.c && rm $a.b.c, $a.b?.c would produce an explicit null if $a.b?.c does not exist, and && would only run the rm if $a.b?.c exists.

Ah, the sexiness from Kotlin. Yes. That's nice.

Now I get your point regarding the explicit null being a "failure". This makes sense, now.

Although, I guess I am biased from Kotlin. In Kotlin null is a neutral feature. It's not "bad" or "good". It's just another tool in the toolbox. For example, in Kotlin you would ask, whether it is null or not, essentially emitting a Boolean value, which naturally leads to a very clear & indisputable "success" or "failure" result.

I'm not for plain copying of behaviour from other languages, but I'm also unsure, whether an explicit null like this is always "bad" in Nu, too. I guess, it's not even about "bad" or not -- it's more about how we want || & && to feel & behave like.

I just notice, that from your examples $a.b?.c || rm $a would basically be equivalent to something along the lines of m.name ?: return null, which seems awesome. Though, this is again my Kotlin bias speaking.

Anyway, thank you for the information about the behind the scenes. It's interesting to know & think about.

@fdncred
Copy link
Collaborator

fdncred commented Feb 15, 2024

Is it possible to move forward with this? By that, I mean, do we understand how we want everything to work with && and || and ;?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
error-handling How errors in externals/nu code are caught or handled programmatically (see also unhelpful-error) semantics Places where we should define/clarify nushell's semantics
Projects
None yet
Development

No branches or pull requests

6 participants