Make memoized function calls recognize equivalent input sequences (and so avoid redundant cache entries)

In the demo below, we see that the memoized function func() executes twice out of the three times it is called. Ideally, it would only execute upon the first call, but the memoization engine fails to recognize that (1) and (2) are equivalent calls. Is there any way to get a memoized function to ignore differences in the ordering of Name/Value arguments?
mf=memoize(@func);
mf('A',1,'B',2) %(1) Cached
Executing
mf('B',2,'A',1) %(2) Cached
Executing
mf('A',1,'B',2) %(3) Not Cached
function func(opts)
arguments
opts.A
opts.B
end
disp("Executing")
end

2 commentaires

As you've written them (1) and (2) are equivalent, but that's not necessarily true in general. The ordering of the name-value arguments can matter, particularly in the case where one or more names are repeated.
f1 = figure(Color = "r", Color = "g");
f1.Color % Green figure
ans = 1×3
0 1 0
<mw-icon class=""></mw-icon>
<mw-icon class=""></mw-icon>
f2 = figure(Color = "g", Color = "r");
f2.Color % Red figure
ans = 1×3
1 0 0
<mw-icon class=""></mw-icon>
<mw-icon class=""></mw-icon>
No, this behavior is not a bug. It can be useful in a case where you want to fix a particular name-value argument in a function you're writing but allow users to override that by specifying the same argument when they call your function.
createFigure = @(varargin) figure("Color", "r", varargin{:});
If I call createFigure with no inputs, it makes a red figure.
f3 = createFigure();
f3.Color % Red figure by default
ans = 1×3
1 0 0
<mw-icon class=""></mw-icon>
<mw-icon class=""></mw-icon>
But I (or a user of my createFigure function) can override this. They don't need to know or care which arguments I used to call the figure constructor.
f4 = createFigure(Color = "g");
f4.Color % Green figure
ans = 1×3
0 1 0
<mw-icon class=""></mw-icon>
<mw-icon class=""></mw-icon>
Memoization treating the lines of code that define f1 and f2 as the same would be incorrect.
I used an object constructor above, but specifying a repeated name-value argument works the same way with a regular function.
func(A = 1, B = 2, A = 3)
Executing A: 3 B: 2
I believe objects can make this type of scenario more likely to happen, particularly if you have Dependent properties or properties that interact with one another like some of the "Mode" properties of some of the graphics objects (explicitly setting the XLim property of an axes changes the XLimMode property of that axes from 'auto' to 'manual', for example.) The Units and Position properties of Handle Graphics objects also interact and can depend on the order in which they're specified.
function func(opts)
arguments
opts.A
opts.B
end
disp("Executing")
disp(opts)
end
Matt J
Matt J le 18 Juin 2026 à 16:44
Modifié(e) : Matt J le 18 Juin 2026 à 16:52
Thanks @Steven Lord. That is an important edge case. I guess what I really wanted is for memoization to be based on the results of argument block processing rather than the original input argument sequence. That's what this wrapper workaround accomplishes and that would handle the case you raise as well.
I have now also submitted this as an enhancement request (Support Case #0880592), so that hoepfully the workaround will eventually be unnecessary.

Connectez-vous pour commenter.

 Réponse acceptée

Paul
Paul le 18 Juin 2026 à 0:49
Modifié(e) : Paul le 18 Juin 2026 à 1:03
That's interesting and seems to suggest that the input argument list is saved off to some data structure (like a cell array?) before doing anything else, rather than filling in opts first and then saving opts in the data structure that is cached.
Perhaps a workaround would be write a wrapper function that takes the same positional and optional arguments as func, sorts the optional arguments in some way, and then passes the positional and sorted optional arguments to mf. Unfortunately mf is scoped like any other variable, so has to be passed into the wrapper function (or could be made persistent in the wrapper function with whatever risk/annoyance that might entail).
clearvars
clearAllMemoizedCaches
mf = memoize(@func);
funcwrap(mf,'A',1,'B',2) %(1) Cached
Executing
funcwrap(mf,'B',2,'A',1) %(2) Cached
funcwrap(mf,'A',1,'B',2) %(3) Not Cached
funcwrap(mf,'A',5)
Executing
funcwrap(mf,'A',5)
funcwrap(mf,'B',10)
Executing
funcwrap(mf,'B',10)
funcwrap(mf)
Executing
funcwrap(mf)
function funcwrap(mf,varargin)
if numel(varargin) == 2
v = varargin;
else
v = horzcat(sortrows(reshape(varargin,[],2)')'); % assuming all arguments are optional
end
mf(v{:});
end
function func(opts)
arguments
opts.A
opts.B
end
disp("Executing")
end

5 commentaires

Unfortunately mf is scoped like any other variable...
It's not! So the following scheme, analgous to feval, would work. Unfortunately, it breaks when the positional arguments have defaults :-(
clc, clearAllMemoizedCaches, clear
clearvars
clearAllMemoizedCaches
mfeval(@func,10,'A',1,'B',2) %(1) Cached
10
mfeval(@func,10,'B',2,'A',1) %(2) Cached
mfeval(@func,10,'A',1,'B',2) %(3) Not Cached
mfeval(@func,20,'A',5)
20
mfeval(@func,20,'A',5)
mfeval(@func,30,'B',10)
30
mfeval(@func,30,'B',10)
mfeval(@func,40)
40
mfeval(@func,40)
function mfeval(fun,varargin)
%feval for memoized function
N=abs(nargin(fun));
posargs=varargin(1:N); %positional argument (Assumes no defaults)
v=varargin(N+1:end);
if ~isempty(v)
v = sortrows(reshape(v,2,[])')';
end
mf=memoize(fun);
mf(posargs{:},v{:});
end
function func(pos,opts)
arguments
pos;
opts.A
opts.B
end
disp(pos)
end
I think mf is scoped like a normal variable, but what I missed is that that cache associated with func is global-ish and is shared with any mf's that point to @func. At least I think that's what's going on.
I think I see the problem with positional arguments that have defaults and therefore might or might not be passed to mfeval.
Maybe the new metafunction is helpful (new in 2026a, I see this post is tagged with 2024b :( )
metaf = metafunction("func")
metaf =
Function with properties: Name: 'func' Description: '' DetailedDescription: '' FullPath: '/tmp/Editor_xfojm/LiveEditorEvaluationHelperEeditorId.m' NamespaceName: '' Signature: [1×1 matlab.metadata.CallSignature]
metaf.Signature
ans =
CallSignature with properties: Inputs: [1×3 matlab.metadata.Argument] Outputs: [1×0 matlab.metadata.Argument] HasInputValidation: 1 HasOutputValidation: 0
metaf.Signature.Inputs
ans = 1×3 Argument array with properties:
Identifier Description DetailedDescription Required Repeating NameValue Validation DefaultValue SourceClass
metaf.Signature.Inputs.NameValue
ans = logical
0
ans = logical
1
ans = logical
1
metaf.Signature.Inputs([metaf.Signature.Inputs.NameValue]).Identifier
ans =
ArgumentIdentifier with properties: Name: 'A' GroupName: 'opts'
ans =
ArgumentIdentifier with properties: Name: 'B' GroupName: 'opts'
It seems like the Name field would be enough information to determine which elements of varargin to mfeval are name-value pairs and deal with them, and anything else is positional, which get passed as is.
function func(pos,opts)
arguments
pos = 1; % with default
opts.A
opts.B
end
disp(pos)
end
Matt J
Matt J le 18 Juin 2026 à 2:50
Modifié(e) : Matt J le 18 Juin 2026 à 3:05
It seems like the Name field would be enough information to determine which elements of varargin to mfeval are name-value pairs and deal with them, and anything else is positional, which get passed as is.
Indeed! I've been a bit hesitant to upgrade past 2024b, though. The new desktop sacrificed some features that I like.
Perhaps a workaround would be write a wrapper function that takes the same positional and optional arguments as func, sorts the optional arguments in some way, and then passes the positional and sorted optional arguments to mf.
Here, I thought you were talking about the scheme below. I think this probably requires the least gymnastics in terms of handling mixtures of positional and name/value arguments, even if a different wrapper would have to be provided for every function you desired to memoize.
clearvars
clearAllMemoizedCaches
funcwrap('A',1,'B',2)
Caching 10
funcwrap('B',2,'A',1)
funcwrap('A',1,'B',2)
funcwrap(3,'A',5)
Caching 3
function funcwrap(pos,opts)
arguments
pos=10;
opts.A
opts.B
end
optCell=namedargs2cell(opts);
mf=memoize(@func);
mf(pos,optCell{:});
end
function func(pos,opts)
arguments
pos=10;
opts.A
opts.B
end
disp("Caching")
disp(pos)
end
"I thought you were talking about the scheme below. "
Nope, but I wish I had thought of it.

Connectez-vous pour commenter.

Plus de réponses (0)

Catégories

En savoir plus sur Performance and Memory dans Centre d'aide et File Exchange

Produits

Version

R2024b

Question posée :

le 17 Juin 2026 à 21:37

Modifié(e) :

le 18 Juin 2026 à 16:52

Community Treasure Hunt

Find the treasures in MATLAB Central and discover how the community can help you!

Start Hunting!

Translated by