adobe / json-formula

Query language for JSON documents
http://opensource.adobe.com/json-formula/
Apache License 2.0
19 stars 8 forks source link

Add a debug() function #140

Closed JohnBrinkman closed 4 months ago

JohnBrinkman commented 5 months ago

It can be difficult to debug complex chained expressions. To help, we propose a new function:

debug(expression, [returnValue])

A host application will use the debug() function to emit strings for the user to see. e.g. to the console in a browser.

With a single parameter, the debug function acts as a "pass-through" function where the input value is displayed for debugging and also returned by the function. This makes it easy to chain the debug function into the middle of an expression.

e.g. given the expression:

(
 (
  items[*].price * 
  items[*].quantity
 ).sum(@) * tax
).round(@, 2)

We can debug the sum() portion of the expression using:

(
 (
  items[*].price * 
  items[*].quantity
 ).sum(@).debug(@) * tax
).round(@, 2)

Which would emit something like: 10.48 to the debug output.

Or we can add context by turning the first parameter into a more human readable string, and adding a second parameter for a return value:

(
 (
  items[*].price * 
  items[*].quantity
 ).sum(@).debug("Sum: " & @, @) * tax
).round(@, 2)

Which would emit something like: Sum: 10.48 to the debug output.

You can add debug() multiple times in the same expression:

(
 (
  debug(items[*].price) * 
  debug(items[*].quantity)
 ).sum(@).debug("Sum: " & @, @) * tax
).round(@, 2)

which would output something like:

[3.23,1.34]
[2,3]
Sum: 10.48
Eswcvlad commented 5 months ago

Maybe something like debug(expression, [format_expr]) would be better? So that you can change log output in a non-chained call scenario: debug(sortBy(x, &@.key), &("Sorted values: " & toString(@))). In the current implementation you would need to write and run the expression twice in such cases.

JohnBrinkman commented 5 months ago

Hmm. Interesting idea:

The tradeoffs are:

If we keep the original syntax, then we can evaluate the expression only once by using a chained expression: sortBy(x, &@.key).debug("Sorted values: " & toString(@), @)

If we changed the method signature -- and used chained expressions, then using lambda feels awkward: sortBy(x, &@.key).debug(@, &"Sorted values: " & toString(@))

Suppose we wanted to debug x before and after sort. Using nested expressions:

debug(sortBy(debug(x, &"Before sort"& toString(@)), &@.key), &"After sort: "& toString(@))

vs.

x.debug("Before sort: "& toString(@), @).sortBy(@, &@.key).debug("After sort: "& toString(@), @)

I prefer to optimize debug() for usage via chained expressions. It avoids nested expressions inside a function and makes it easier to strip out the debug() calls once the expression works.

Eswcvlad commented 5 months ago

Not sure, I quite agree on it being awkward. It is just one & longer. Kind of like an inline log handler.

As for the parameter order, I preferred to switch, as in the current version it looks as if first argument changes its meaning based on argument count (i.e. from a return value to a formatting expression), which is a bit odd.

There can also be a tiny performance benefit in the lambda version. In a web context it makes sense to just do console.log by default, but in others (like a native library) it would make more sense to not print anything, unless an explicit handler is passed. So in that case the formatting expression won't be processed. I don't think this is, really, a relevant point, as you wouldn't want to have debug left in the final expression to begin with, but you never know...

I prefer to optimize debug() for usage via chained expressions. It avoids nested expressions inside a function and makes it easier to strip out the debug() calls once the expression works.

Agreed, but it is, mostly, a style preference. LISP-enjoyers might not agree with this :)

If the cost is negligible, might be worth to make both styles viable.

JohnBrinkman commented 5 months ago

Not sure, I quite agree on it being awkward.

I should have been more clear: I think lambda parameters are an advanced feature of the language. While debug() is going to be used a lot by novice users.

might be worth to make both styles viable.

What would that look like?

debug(expression, [format_expr])

Where expression is any type (except lambda) and is always used as the return value, and if there's no second parameter is displayed using toString()

format_expr is any type (including lambda) that's always used for debug display, and if it's a lambda, uses the first parameter as context.

That could work

JohnBrinkman commented 4 months ago

Approved