9

I would like a function (for example, a fit function) to return an anonymous function (usually stored in a struct) that I can save and use later. However, passing @func tends to pass a function pointer rather than the function itself. Is an inline function the only way to do this? I would like to avoid inline because it is extremely slow.

If that question is not clear, here is a sample of problematic code: I write a testFunc.m file in some PATH

    %testFunc.m
    function myfunc = testFunc()
        myfunc = @(x) x.^2;
    end

I then store the function in a struct. (I know this really should be an object!)

    >> mystruct = struct;
    >> mystruct.func = testFunc()
    >> mstruct.x = [1 2 3];
    >> save('myfile.mat','mystruct')
    >> mystruct.func(mystruct.x)

    ans = 

         1     4     9

If I then move myfile.mat or testFunc.m and load myfile.mat, I cannot load the old struct. Instead, I get the error:

    >> cd 'otherdir'
    >> load('../myfile.mat')

    Warning: Could not find appropriate function on path
    loading function handle PATH/testFunc.m>@(x)x.^2 

I know there is a problem because, if I check functions

    >> functions(mystruct.func)

    ans = 

         function: '@(x)x.^2'
             type: 'anonymous'
             file: 'PATH/testFunc.m'
        workspace: {2x1 cell}

Is there some way to strip off the file workspace information? Are inline functions the only solution?

Arthur Ward
  • 597
  • 3
  • 20
emarti
  • 172
  • 2
  • 8
  • I think I understand the point a bit better -- thanks for all the discussion! For the time being, I'm using inline functions, even though they are slow: %testFunc.m function myfunc = testFunc() myfunc = inline('x.^2'); end – emarti Feb 15 '12 at 00:58
  • Ah - if you just need functions that can be expressed as inlines, I think you can use "saniitized" anonymous functions from a common, clean workspace as a drop-in replacement for `inline()` and avoid the slowdown; see my updated answer. – Andrew Janke Feb 15 '12 at 15:35
  • Also, you can use addpath() to manage your path if you're not already. This lets you keep your source code on the Matlab path regardless of your current directory. – Andrew Janke Feb 15 '12 at 19:36

4 Answers4

8

The Simple Case

If the functions you want to be anonymous are limited to being defined just in terms of their input parameters (like inline functions are), and you can commit to keeping one function on your path, then you can make "sanitized" anonymous functions.

function out = sanitized_anon_fcn(str)
out = eval(str);
end

So, in your code, where you want to make an anonymous function, do this.

%testFunc2.m
function myfunc = testFunc2()
    myfunc = sanitized_anon_fcn('@(x) x.^2');
end

As long as sanitized_anon_fcn.m stays on your path, you can delete testFunc2, and the saved function will continue to work. No special processing needed on save or load. Sanitized_anon_fcn basically works like inline but produces functions that are as fast as anonymous functions (because they are anonymous functions). The speed difference is about 10x in R2011b on my computer.

The General Case

In the general case, where the functions might actually use variables from their workspace, things get trickier.

Caveat: This is a bit of a sick hack, and I do not endorse its use in production code. But as an example of how the language works, I can't resist posting it.

I think you're 90% there already. But you need to preserve the workspace info instead of stripping it off, because it may contribute to the operation of the function. Instead of saving the anonymous function handle, grab the output of that functions() call you're making and save that.

fcn = testFunc();
fcn_info = functions(fcn);
save willbreak.mat fcn
save blah.mat fcn_info

Then load it back. You'll still get the same warning, but now the warning applies only to function handles captured inside the workspace of your top-level anonymous function. If your function doesn't actually reference them (and it shouldn't), you can ignore the warning and it'll work.

s0 = load('willbreak.mat')  % will warn and return unusable function
warning off MATLAB:dispatcher:UnresolvedFunctionHandle
s = load('blah.mat')  % will warn, but the first-level function will be usable
warning on MATLAB:dispatcher:UnresolvedFunctionHandle

Then pass it to something like this function which will bring your anonymous function back from the dead in a new workspace with the same workspace values, more or less.

function out = reconstruct_anon_fcn(s)

for iWks = 1:numel(s.workspace)
    wkspace = s.workspace{iWks};
    varnames = fieldnames(wkspace);
    for i = 1:numel(varnames)
        tmp = wkspace.(varnames{i});
        eval([varnames{i} ' = tmp;']);
    end
end

fcn_str = s.function;
fcn = eval(fcn_str);
out = fcn;
end

In our example case:

fcn = reconstruct_anon_fcn(s.fcn_info)
fcn(2)   % and it works!

Now, all the loaded anonymous functions will claim to be from this new file, but it shouldn't matter, because it's just the snapshotted state of the workspace, not closed-over variables, that is used by anonymous functions. And in the case where there were anonymous function handles in the workspace that actually were used by the computation, you'll get an appropriate error saying "Undefined function handle".

This is a hack, but maybe you could take this and extend it in to something reasonably robust.

Andrew Janke
  • 23,508
  • 5
  • 56
  • 85
  • Sorry I could not chat with you yesterday. I was kind of sleepy :) Anyway your hack looks fine, as long as you understand the limitations. – Andrey Rubshtein Feb 15 '12 at 11:35
2

It is not possible, as it violates the concept of closure. Imagine that you define your anonymous function in that way:

   function f = LocalFunc()
       y = 3;
       f = @(x)(x+y);
   end

How can someone know about the fact that y was 3, besides saving the workspace info?

Edit(1) One might say that you can capture the value of the variable. But that is not always true. Imagine the following situation - You have a handle class, which has a method that adds to the input some property.

classdef Foo  < handle    
    properties
        DX;
    end

    methods
        function y = AddDX(this,x)
            y = x+ this.DX;
        end        
    end
end

Now you create a function that creates anonymous function that calls the method:

function fOut = TestConcept(handleClass)
    handleClass.DX = 3;    
    fOut = @(x)(handleClass.AddDX(x));
end

Now you create a new Foo() , and pass it to the TestConcept func:

 handleClass = Foo();
 fOut = TestConcept(handleClass);
 handleClass.DX = 3;
 fOut(4)

The result is 7

And then change the handle, and call it again:

 handleClass.DX = 100;
 fOut(4)

The result is 104. handleClass value could not have been saved, since it is not a value type, it is a handle.

As you can see, you cannot always capture the values, sometimes you have a reference.

Andrey Rubshtein
  • 20,795
  • 11
  • 69
  • 104
  • Not quite true: anonymous functions are not closures; they capture the *value* in the variables at the time of the anonymous function construction. It is only named nested functions which are actually closures and capture live references to the variables in the enclosing workspace. – Andrew Janke Feb 14 '12 at 23:43
  • @AndrewJanke, you are right of course. But `y` could have been a handle, and you can't capture its value. – Andrey Rubshtein Feb 14 '12 at 23:49
  • @AndrewJanke, please see the updated answer. I wrote a longer explanation. I would like to hear your opinion. thank you! – Andrey Rubshtein Feb 15 '12 at 00:01
  • Ah right. And it's worse - `y` might have been a handle, but the output arg `f` is certainly a handle. – Andrew Janke Feb 15 '12 at 00:04
  • Well. That makes sense. But it doesn't mean you could *never* save an anonymous function handle with its workspaces - you just couldn't if they happened to contain a handle. The same argument applies to cells: in general, a cell array could contain a handle, but we can still save cells as long as the particular cell value we're saving doesn't. I think the same would apply to anonymous function handles with captured workspaces; it's the unfortunate case that the named output argument means that the workspace *will* always contain a handle to anonymous function. – Andrew Janke Feb 15 '12 at 00:11
  • @AndrewJanke, you are right of course. That is why I think that you should undelete your answer, it is quite legitimate IMHO :) – Andrey Rubshtein Feb 15 '12 at 00:13
  • And Matlab will let you save handles like that `Foo` class, and will reconstruct their identity when you load them back in. Like Java serialization. I don't think the problem is being able to capture the workspace values per se, it's that Matlab wants that source file to be present, even though (I don't think) it's actually necessary to reconstruct the anonymous function's behavior. – Andrew Janke Feb 15 '12 at 00:15
  • Aw thanks. :) Well, it's still busted - for it to work, you need to go in and knock out the anonymous fucntion handles from the workspaces. Which you can do, but at that point it's a sick enough hack I'm not sure I'm comfortable endorsing it in public... – Andrew Janke Feb 15 '12 at 00:22
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/7707/discussion-between-andrew-janke-and-andrey) – Andrew Janke Feb 15 '12 at 00:40
1

I have encountered this problem before as well. str2func is almost identical to the handle operator (@) -- it does not create the file association, nor does it retain workspace variables. In your case:

myfunc = str2func('@(x) x.^2');

Try filing this behavior as a bug with the MathWorks, as it really undermines a very useful application of anonymous functions. If enough of us complain, they might fix it.

Arthur Ward
  • 597
  • 3
  • 20
0

Expanding on the suggestion by @ArthurWard:

To save simple inline functions from your code environment, try

myStruct.myFunc = str2func(func2str( myFunc ));

save('myStruct.mat',myStruct);

func2str() turns your function into a literal string, and str2func() turns it back into a handle which matlab treats as free of dependencies.

You can tidy it up by making naming the operation something like

export_function = @(h) str2func(func2str( h ));

Any function that can survive func2str() should work.

petorr
  • 1
  • 1