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

A way to control unbound variable errors from expressions? #236

Open
vlazar opened this issue Mar 24, 2021 · 4 comments
Open

A way to control unbound variable errors from expressions? #236

vlazar opened this issue Mar 24, 2021 · 4 comments

Comments

@vlazar
Copy link
Contributor

vlazar commented Mar 24, 2021

Dentaku has both

  • bang methods like evaluate! which raise unbound variable errors
  • and non bang like evaluate which return nil values

I want to start a discussion on adding the same ability to chose on the expressions level.

I have a system where I don't have full control over incoming data format. I also want system to be flexible and be able to fix issues in incoming data with Dentaku expressions instead of custom Ruby code.

Currently I'm using PLUCK to map incoming array of hashes to array of simple types like strings, numbers, etc. When PLUCK tries to get non existent key from hash it wont raise error, but return nil instead:

evaluate!("PLUCK(properties, foo)", properties: [{foo: 1}, {}]) # => [1, nil]

I rely on this behavior of no raised error on missing key with expressions being a gateway between the incoming data and the next layer of expressions. This is where I can stop unbound variable errors.

With recent fix ALL/ANY/MAP will now raise an unbound variable error when trying to access key that does not exist for some item in array.

So after this fix PLUCK behaves a bit differently in this regard comparing to ALL/ANY/MAP.

I don't wan't to loose this ability to safely evaluate expression like I currently do with PLUCK.
At the same time I feel like PLUCK should be fixed too to behave similar to ALL/ANY/MAP as PLUCK(properties, foo) semantically is just an alias for MAP(properties, p, p.foo).

So I wanted to start a discussion of how (or should) Dentaku have some way in expression to chose:

  • an implicit strict behavior (the current one) in regards to unbound variable errors
  • explicit behavior where I can tell Dentaku: I understand there could be unbound variables here, and here is what Dentaku should return in such cases

Essentially something similar to safeguards like IF(var, var, 0) where with var = nil you'll get 0 instead of some raised error (I know that evaluate!("IF(var, var, 0)") would raise the same unbound error and only evaluate!("IF(var, var, 0)", var: nil) works).

I can currently use PLUCK(), but I feel like it needs to be fixed to behave the same as MAP alias and that would leave me without ability to mitigate unbound variable errors at the expression level.

Also PLUCK is convenient for arrays, but to mitigate unbound variable errors for just a hash I would need this workaround:

evaluate!("PLUCK({props}, foo)", props: {}) => [nil]

It does the job but does not look pretty.

@vlazar
Copy link
Contributor Author

vlazar commented Mar 24, 2021

Also somewhat related to this issue #234

@vlazar
Copy link
Contributor Author

vlazar commented Apr 4, 2021

@rubysolo Just to demonstrate even uglier syntax I currently use to safely get a default value for a single value in single expression to guard against Dentaku::UnboundVariableError:

# defaults to 0 when `input.bar` is missing
c.evaluate!("IF(PLUCK({input}, bar)[0] != NULL, PLUCK({input}, bar)[0], 0)", input: {}) # => 0

# or returns value if there is `input.bar`
c.evaluate!("IF(PLUCK({input}, bar)[0] != NULL, PLUCK({input}, bar)[0], 0)", input: { bar: 123 }) # => 123

In addition to syntax building intermediary arrays is also something I want to get rid off.

@vlazar
Copy link
Contributor Author

vlazar commented Apr 6, 2021

I wonder if the value missing from calculator's memory and not provided for solve could be treated the same as special :undefined value which is returned by non bang solve.

Say this result could be the way I have a list of unbound variables (e.g. to distinguish from just nil values)

c.solve(x: "not_provided") # => {:x=>:undefined}

Then it can be used as context for other calculations like this guard conditional (but it's not possible currently, unless I miss something).

# currently gives error since :undefined is Symbol and it's truthy
# Dentaku::AST::Multiplication requires operands that respond to * (Dentaku::ArgumentError)
c.store({:x=>:undefined}) { c.evaluate!("IF(x, x * 2, 0)") }

I wonder if having syntax support for symbols (or just a single Symbol :undefined for my sue case) would make sense. So that I can do something like directly:

c.store({:x=>:undefined}) { c.evaluate!("IF(x != :undefined, x * 2, 0)") } # => 0

@kapcod
Copy link

kapcod commented Feb 20, 2024

I know it's 3y old, but it's still open for some reason so I assume it's still relevant.
Since you can always add a function, you can do add_function(:has_key, :boolean, ->(h, f){ h.key?(f) || h.key?(f.to_sym) }) and then you can use this approach in map if needed and pluck then is just syntax sugar for map(ar, if(has_key(a,"x"), a.x, null).
Alternative syntax would be to add fetch(hash, key, default) function.

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

No branches or pull requests

2 participants