Logical short-circuit inside a function handle
E

1

6

I have a function handle that operates on 2d arrays of arbitrary size:

R2T = @(DL1,DL2) arrayfun(@(DL1,DL2)...
                 1/(fzero(@(x)fFitObj1(x)./fFitObj2(x)-...
                 DL1./DL2,[minLim maxLim])) ...
                 ,DL1,DL2) - C1;

Here's a bottom-up breakdown of what it does:

  • fzero(@(x)fFitObj1(x)./fFitObj2(x)-DL1./DL2,[minLim maxLim]) - This bit looks for a zero of the considered function on the interval [minLim maxLim], where fFitObj1 and fFitObj2 are function handles available from before, C1 is some known constant and DL1, DL2 are provided.
  • @(DL1,DL2)1/(fzero(...)) - a wrapper for fzero that allows DL1 and DL2 to be provided from outside.
  • arrayfun(@(DL1,DL2)...,DL1,DL2) - another wrapper which allows fzero to correctly operate element-by-element when DL1, DL2 are provided as a matrix.
  • R2T = @(DL1,DL2) arrayfun(...) - C1; - yet another wrapper that allows to provide DL1, DL2 from outside.

My problem is that sometimes the matrices DL1, DL2 may contain NaN values, in which case fzero returns the following error:

Error using fzero (line 242)
Function values at interval endpoints must be finite and real.

This is why I naturally thought of the available operations with short-circuiting, so I tried to incorporate a any(isnan([DL1,DL2])) into this so that fzero won't even be evaluated if its inputs would be NaN - but whatever I try (e.g. a custom-made ternary operator) the fzero seems to be evaluated and the code errors.

The desired result: I'd like to implement a lazy evaluation of the fzero to only occur when the inputs are valid (in this case, not NaN), and return NaN otherwise as demonstrated in the Edit below.

Related resources:


Edit:

Here's a piece of code that illustrates the problem (MATLAB 2014a):

clear variables; clc;

LIM = [0 5];
fFitObj1 = @(x)x.^2; fFitObj2 = @(x)1;
C1 = 100;

[DL1A,DL2A,DL1B] = deal(ones(2));
DL1B(4) = NaN; DL2B = DL1B;

R2T = @(DL1,DL2) arrayfun(@(DL1,DL2)...
                 1/(fzero(@(x)fFitObj1(x)./fFitObj2(x)-...
                 DL1./DL2,LIM)) ...
                 ,DL1,DL2) - C1;
             
R2T(DL1A,DL2A) %//case A, runs fine
%{
// ans =
// 
//   -99   -99
//   -99   -99
%}   
R2T(DL1B,DL2B) %//case B, errors due to NaN
%{
// Error using fzero (line 242)
// Function values at interval endpoints must be finite and real.
// 
// Error in @(DL1,DL2)1/(fzero(@(x)fFitObj1(x)./fFitObj2(x)-DL1./DL2,LIM))
//
//
// Error in @(DL1,DL2)arrayfun(@(DL1,DL2)1/(fzero( .....
%}

The desired result, in case B is:

 ans =
 
   -99   -99
   -99   NaN
Ezzell answered 1/3, 2015 at 19:6 Comment(8)
Have you considered using a separate .m file instead?Byelaw
Yes, I have.. My hunch says that this is too dynamic to be put in an m-file (I may be wrong there, though).Ezzell
Well, .m files are more flexible than anonymous functions, so it must be possible (the process of capturing variables via arguments may be less elegant, but you can use if-else as well as try catch, which would be a huge benefit in this case)Byelaw
Putting it all in a regular M-file will be faster and easier to read too. Stop the madness. You could possibly also replace the arrayfun with something faster too.Jenny
I understand what you're saying.. Right now I'm trying to find out whether this is possible with function handles.Ezzell
Well, it is in fact possible to short-circuit in anonymous functions, but it is difficult for us to find out what is actually wrong with your code, as your example is not a mcve. (We don't have fFitObj1, minLim,...)Byelaw
You can put anything you like there, really (e.g. '@()1').. I'll add a runnable example when I'm near a computer.Ezzell
There is a happy compromise between a separate m-file and an anonymous function -- a nested function. The advantage of the nested function is that it can close over the local variables (like minLim and maxLim) in the current scope, just like an anonymous function can. But it also can use normal statement blocks, it doesn't have to be just one single expression.Absinthism
B
5

As already mentioned in the comments: Doing this inline is madness and you are much better off using a separate function / .m file.

It will be

  • Faster
  • Easier to read
  • Easier to write
  • Easier to debug

You could do this for example in a similar way to this:

function out = R2TComputation(DL1, DL2, minLim, maxLim, C1)
...%Compute whatever R2T would compute.

To get the same interface as your original anonymous function has, you can simply create

R2T = @(DL1, DL2) R2TComputation(DL1, DL2, minLim, maxLim, C1)

which will capture the current values of minLim, maxLim and C1 at the time you create this handle R2T.


Yet another option would be to use a nested function instead of the external one. It would have access to the parent function's variables, yet still be able to use if, else and all the other basic tools you need. Only downside: It is not meant to be accessed from within other files.

... % Main function stuff
     function out = R2T(DL1, DL2)
         if ...
            out = ...
         ...
     end
... % Use R2T ...

However, for the sake of freedom of shooting oneself in the foot, here is an inline version of if-else, which I wrote in the spirit of Loren's blog post and I do not recommend using, as there are hardly any benefits of using a single expression instead of the corresponding if-else statements.

ifelse = @(cond, varargin) varargin{1+~cond}(); %Only for the insane

If you want it to do lazy evaluation, you need to pass an anonymous function with zero parameters, which ifelse will then evaluate (That's what the last two parentheses () in ifelse are for):

ifelse(true, 42, @()disp('OMG! WTF! THIS IS CRAZY!!111'))

If you simply wrote the function call to disp as an argument to ifelse without @(), the function would be called before we even access ifelse. This is because MATLAB (as most other languages) first computes the return value of the function, which is then passed to ifelse as a parameter.

In your case the resulting code would be:

R2T = @(DL1,DL2) arrayfun(@(DL1,DL2)...
                 ifelse(~any(isnan([DL1, DL2])), ...
                        @() 1/(fzero(@(x)fFitObj1(x)./fFitObj2(x)-DL1./DL2,LIM)), ...
                        NaN), ...
                 DL1, DL2) - C1;
Byelaw answered 1/3, 2015 at 20:24 Comment(12)
It also won't work, because minLim, maxLim, and C1 won't be in scope. You could use globals (yuck!), or add more parameters (making it incompatible with passing through existing functions that expect a certain signature), but using a nested function would provide most of the benefits with none of the limitations.Absinthism
Ok, another option would be to make a separate m function that replaces just the fzero and NaN-checking part, since that doesn't require any additional external inputs.Absinthism
@BenVoigt: I'm not sure I get what you mean. R2T would capture minLim, maxLim and C1. What do you mean wouldn't work?Byelaw
Aren't you suggesting moving R2T to a separate m-file? It can't then capture anything.Absinthism
Nah, this is SPARTA!! :)Ezzell
@BenVoigt: Yes, this what I would suggest. Creating a function out=R2T(DL1, DL2, minLim, maxLim, C1) and then building an anonymous function using: R2Tanon = @(DL1, DL2) R2T(DL1, DL2, minLim, maxLim, C1), capturing the current values of minLim, maxLim and C1. I'll add it to the answer.Byelaw
@BenVoigt: Your suggestion of a nested function is also a great idea. It just depends if the OP will want to do this computation across multiple files I guess, which would be the only case where my version is preferable to nested functions.Byelaw
(For future readers: knedlsepp's last comment was about a comment of mine which was deleted, since I found and edited into the answer a link to Loren's blog that discusses this topic. My question was "How come ifelse(true, 42, @()fzero(123,456)) works but ifelse(true, 42, fzero(123,456)) doesn't?")Ezzell
@Byelaw - Great. I think a more intuitive explanation is that when the argument is an anonymous function, whatever you write there is practically equivalent to a string (and therefore has no effect). If there comes a time for it to be evaluated, something like eval() or str2func() is happening internally.Ezzell
@Dev-iL: You are basically right, with the addition that local variables that the anonymous function uses will be copied (captured) to the anonymous functions own workspace.Byelaw
@SamRoberts: It was my intention to hide the code to make it more obvious that the solution is not something I would recommend. Don't you agree with using a spoiler here? I don't want to spoil people's programming experience. ;-)Byelaw
@Byelaw My apologies - I didn't realise you'd done that deliberately. I guess I thought spoilers are better used for spoilers, and if you're posting code that you wouldn't recommend, it might be clearer to just say "I don't recommend this". But that's just an opinion - if your styling was deliberate, please feel free to roll back my edit.Nadler

© 2022 - 2024 — McMap. All rights reserved.