-
Notifications
You must be signed in to change notification settings - Fork 5
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
Fix argument evaluation issue in conditional expressions #218
Fix argument evaluation issue in conditional expressions #218
Conversation
Previously, expressions like: `@if(status,\nLEFT(status, 2),\nfalse)` would error out due to evaluating all arguments, even when not needed. This behavior prevented conditional checks like "if X is nil, do this otherwise do that". With this change, argument evaluation is fully delegated to each function, allowing proper conditional checks. The custom callback function is now set as a custom context attribute, enabling nested functions to use it as needed.
What do you think @smn ? Is this acceptable? |
@@ -70,9 +69,8 @@ defmodule Expression.Eval do | |||
def eval!({:function, opts}, context, mod) do | |||
function_name = opts[:name] || raise "Functions need a name" | |||
args = opts[:args] || [] | |||
arguments = Enum.reduce_while(args, [], &args_reducer(&1, function_name, context, mod, &2)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It should also make the library a lot faster because now it can short-circuit
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it's interesting as the intention of using reduce while here was to only evaluate until one of the things resolved to true. I'm happy that it's now solved but I'm unsure why it's solved. I'm going to need to spend some more time on this to make sure I understand.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems like the only reason to do this here is:
- avoid issues with the "or" but I also don't understand why fixing it here
- avoid having to pass the custom callback as argument of every function both in the standard lib and custom callback module functions
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given that the custom callback module does not change between function calls, I didn't see it as problematic to add it here and so pass it along every call to eval!
in the Expression.Callbacks.EvalHelpers
. Just like we store temporary values in the context with __value__
, __captures
, __type__
etc.
It is basically the implementation of the state monad
{:ok, ast, "", _, _, _} = | ||
Parser.parse("@if(status,\nLEFT(status, 2),\nfalse)") | ||
|
||
assert false == |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Without this change, this AST will break because it will try to "LEFT" "nil" but this is not needed because if will evaluate to false and so return it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for this contribution! I was looking into a similar problem.
Could we extend this solution to and
/or
boolean evaluations? I've been trying a lazy evaluation of and
with a nil
value, but it seems to be failing. Here’s an example test:
test "and with nil values" do
{:ok, ast, "", _, _, _} = Parser.parse("@and(isstring(status), LEFT(status, 2))")
assert false == Eval.eval!(ast, %{"status" => nil})
end
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @Djack1010, I don't think there is a good way to fix the "and" because unfortunately every argument of the function needs to be evaluated and not return error in order to check if the function can return true or false. This fix would only work for OR and IFs
|> Expression.Eval.eval!(ctx) | ||
|> Expression.Eval.eval!( | ||
ctx, | ||
Map.get(ctx, "__CUSTOM_CALLBACK__", Expression.Callbacks.Standard) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This feels like we need to give this some more thought, it's an entirely different means of specifying the callback module. In other places we're passing it in explicitly as an optional argument.
Maybe this helper just isn't useful enough to be defined in a separate module, would that prevent us having to supply the callback module as an argument? Especially because the eval! function is used in the callback module primarily.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assumed that this was done before the implantation of “or” in the same way. I think it is fixed because essentially this code was duplicating what “or” is doing in the body of its function 🤔And yes I agree that definining the callback like that changes things quite a bit but I haven’t found a better way to pass it as argument to all functions without changing all of their arity (and so make it also incompatible with all existing callback modules), any idea of how to still solve the issue with the test and keep it as argument?
CLA Assistant Lite bot: I have read the CLA Document and I hereby sign the CLA 1 out of 2 committers have signed the CLA. |
I believe #223 shows that this is fixed, but please confirm. |
@lorenzosinisi I'm closing this PR again but please feel free to re-open if you feel the issue has not been properly addressed. Thanks again for your contribution! |
The goal is to implement "lazy" evaluation of functions, specifically for conditional statements like
@if
. This is particularly useful when dealing with potentiallynil
values.Current behavior:
And the same will go for RIGHT, any
String.split
function etc.If
status
isnil
, this function currently throws an error because all arguments are evaluated eagerly, includingLEFT(status, 2)
, which fails on anil
input.Desired behavior:
The
@if
function should only evaluate the second argument (LEFT(status, 2)
) if the first argument (status
) is truthy. Ifstatus
is falsy (includingnil
), it should immediately return the third argument (false
) without evaluating the second argument.Implementation approach:
Benefits:
nil
values.This approach maintains compatibility with existing functions while adding the capability for lazy evaluation where needed.