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

Unclear or incorrect arithmetic update-assignment documentation #2407

Open
wader opened this issue Feb 2, 2022 · 10 comments
Open

Unclear or incorrect arithmetic update-assignment documentation #2407

wader opened this issue Feb 2, 2022 · 10 comments
Labels

Comments

@wader
Copy link
Member

wader commented Feb 2, 2022

Hi

I noticed something when i tried to use the current value for +=. In the documentation it says:

jq has a few operators of the form a op= b, which are all equivalent to a |= . op b. So, += 1 can be used to increment values, being the same as |= . + 1.

But it seems like for a op= b the input for the RHS is the input of the LHS and not . | a.

$ jq -n '{a:1} | .a += (debug | 1)'
["DEBUG:",{"a":1}]
{
  "a": 2
}
$ jq -n '{a:1} | .a |= . + (debug | 1)'
["DEBUG:",1]
{
  "a": 2
}

In my case I was trying to do something similar to this. Add something to a list conditionally on the current list value:

$ jq -n '{a:[1,2,3]} | .a += [if length % 3 == 0 then 4 else empty end]'
{
  "a": [
    1,
    2,
    3
  ]
}

Here length will get {a:[1,2,3]} as input not [1,2,3] and return 1. But if i use |= i get what i wanted:

$ jq -n '{a:[1,2,3]} | .a |= . + [if length % 3 == 0 then 4 else empty end]'
{
  "a": [
    1,
    2,
    3,
    4
  ]
}

gojq seems to have the same behaviour which makes me think i might misunderstanding something? if so I can try to improve the documentation. If it's a bug it feels it might be a bit too late to change the behaviour?

@itchyny
Copy link
Contributor

itchyny commented Feb 2, 2022

 $ jq -nc '[[0,[1]],[2]] | .[0] += .[1]'
[[0,[1],2],[2]]

 $ jq -nc '[[0,[1]],[2]] | .[0] |= . + .[1]'
[[0,[1],1],[2]]

Yeah, a op= b is not equivalent to a |= . op b. Maybe it's too late. But I feel the both behaviors look intuitively correct and fixing the document is better than breaking things.

@wader
Copy link
Member Author

wader commented Feb 3, 2022

I noticed now that there is at least one a op= b test that would fail with the documented behavior https://github.com/stedolan/jq/blob/master/tests/jq.test#L1016. I guess the tests are as close to a jq specification as there is?

@pkoppstein
Copy link
Contributor

pkoppstein commented Feb 4, 2022

@wader, @itchyny -

It will probably be a long while before the official
documentation is updated, so I was thinking of adding
a section to the jq FAQ. How about this:

Q: What is the difference between |= and the other update-assignment operators?

A: There are three types of update-assignment operators:

  • the simple one: =
  • the fancy one: |=
  • all the others - loosely speaking, the arithmetic update-assignment operators

In the following, we will use += as the examplar or holotype of the
arithmetic update-assignment operators as the basic principle governing
their semantics is the same in all cases except for //=, as noted below.

The update-assignment operators operate on one or more JSON values as
input. To simplify things, let's focus on the case when the input is
a single value, say V. To explain the differences, then, it suffices
to examine the expression:

V | (E op= F)

for the different case of "op=".

In a nutshell:

(0) V | (E = F) is identical to V | (E = (V|F))

(1) V | (E |= F) is identical to V | (E = (V|E|F))

(2) V | (E += F) is identical to V | (E = (V|E) + (V|F))

The caveat is that when evaluating the expression V | E //= F, the current C and Go-based implementations of jq both evaluate F unconditionally, as if by:

V | F as $f || E = (V|E) // $f

Examples:

(0) {"a":1, "b":2} | .a = .b #=> {"a": 2, "b": 2}

(1) {"a": {"b":"B"}} | .a |= .b #=> {"a": "B"}

(2) {"a": 1, "b": 2} | .a += .b #=> {"a": 3, "b": 2}

Executable tests:

def assertEqual(x;y):
  if x == y then empty else [x,y] end;

def t0:
 {"a":1, "b":2}
 | . as $v
 | assertEqual( .a = .b;  .a = ($v|.b));

def t1:
 {"a": {"b":"B"}}
 | . as $v
 | assertEqual(.a |= .b; .a = ($v|.a|.b));

def t2:
 {"a": 1, "b": 2}
 | . as $v
 | assertEqual(.a += .b ; .a = ($v|.a) +($v|.b));

t0,t1,t2

@wader
Copy link
Member Author

wader commented Feb 4, 2022

@pkoppstein Yes would be a good additions i think. I will probably send PR to update to manual also unless someone does it before me. Hopefully it will get merged at some point.

As some trivia I also noticed for //= the RHS is always evaluated even if LHS is not null/false, but guess one can only noticed it by using something with side effects like input, debug etc.

$ jq -n '{a:1} | .a //= (debug | 2)'
["DEBUG:",{"a":1}]
{
  "a": 1
}
$ jq -n '{b:1} | .a //= (debug | 2)'
["DEBUG:",{"b":1}]
{
  "b": 1,
  "a": 2
}

@pkoppstein
Copy link
Contributor

@wader - It seems to me that the behavior of //= that you describe is incorrect and should be fixed, and that it should not be documented except as a bug.

@wader
Copy link
Member Author

wader commented Feb 4, 2022

Yes i was not expecting it to evaluate RHS and after thinking a bit more in regards to jq having functions with side effects i agree it feels like a bug. Would it be resonable to use debug to see if RHS was evaluated or not? i think so.

@itchyny gojq seems to have the same behavior, what do you think?

@itchyny
Copy link
Contributor

itchyny commented Feb 4, 2022

I didn't aware until now but that behavior looks buggy for sure, it should not evaluate RHS just like null coalescing operators in other languages, like C#, JavaScript, and Perl. Note that jq is kind a functional language with purity (with a few exceptions as you would know), and it sometimes has non-standard evaluation order which many people don't notice. jq evaluates RHS first in LHS + RHS but most people don't aware of this.

@wader
Copy link
Member Author

wader commented Feb 7, 2022

Extracted the //= behavior into its own issue so it's not forgotten #2410

@wader
Copy link
Member Author

wader commented Feb 7, 2022

@itchyny I didn't know about the LHS + RHS evaluation order, do you know why that is the case?

@nicowilliams
Copy link
Contributor

@itchyny I didn't know about the LHS + RHS evaluation order, do you know why that is the case?

If I ever knew why, I've forgotten. I think this is a question for @stedolan.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

4 participants