@Memoize
Memoization is an optimization technique primarily used to speed up computer programs by storing the results of expensive function calls and returning the cached result when the same inputs occur again. Arez takes this a step further by making the memoized method observable. The memoized method will be re-calculated any time a dependency is updated as long as there is at least one observer.
To memoize a method within the Arez component model you annotate a method that returns a value with the
@Memoize
annotation in an @ArezComponent
annotated class. The
@Memoize
annotated method is expected to derive the return value from any parameters
passed to the method and/or the return values of other @Observable
or
@Memoize
annotated properties.
The @Memoize
annotation is implemented using computable values.
If the memoized method has no parameters then there is a single ComputableValue
supporting the method.
If a memoized method has parameters then the runtime will create a ComputableValue
instance for each
unique combination of parameters to the method. Each time the memoized method is invoked, it will return the value
cached by the ComputableValue
instance unless the ComputableValue
instance is stale (i.e.
a dependency has changed) or the value of the parameters have changed. If the memoized method is invoked from within
an observer, the observer will be rescheduled any time the memoized value changes.
A basic example:
@ArezComponent
public abstract class CurrencyCollection
{
@Observable
public String getFilter()
{
// Return value used to filter currencies by their symbol
...
}
@Observable
public List<Currency> getCurrencies()
{
// Return the list of all currencies here
...
}
// Memoized method that only changes when a currency is added or removed
@Memoize
public int getCurrencyCount()
{
return getCurrencies().size();
}
// Memoized method that changes when a currency is added or removed,
// or the filter changes
@Memoize
public List<Currency> filteredCurrencies()
{
return getCurrencies()
.stream()
.filter( c -> c.getSymbol().contains( getFilter() ) )
.collect( Collectors.toList() );
}
...
}
A basic example that passes parameters to the memoized method. In this scenario the result of filtering a model is memoized as follows.
@Observable
public String getName()
{
return _name;
}
public void setName( @Nonnull final String name )
{
_name = name;
}
@Memoize
public boolean doesSearchMatch( @Nonnull final String value )
{
return getName().contains( value );
}
Lifecycle Hooks
The component model also supports the definition of callback methods as described in the
computable values document. This feature is currently only available if the memoized method
accepts zero parameters. The callbacks are defined by using the annotations @OnActivate
and @OnDeactivate
. These methods are associated with the
@Memoize
annotated method via naming conventions or through explicit configuration. The
exact requirements for methods annotated by these annotations is defined in the API documentation.
A simplistic example that uses the callbacks to register a listener on the underlying browser objects and updates the computable value is as follows. This is extracted and simplified from an existing component that manages online state.
@ArezComponent
public abstract class NetworkStatus
{
private final EventListener _listener = e -> updateOnlineStatus();
private boolean _rawOnLine = getIsOnLine();
@Memoize
public boolean isOnLine()
{
return isRawOnLine();
}
@OnActivate
void onOnLineActivate()
{
WindowGlobal.addOnlineListener( _listener );
WindowGlobal.addOfflineListener( _listener );
}
@OnDeactivate
void onOnLineDeactivate()
{
WindowGlobal.removeOnlineListener( _listener );
WindowGlobal.removeOfflineListener( _listener );
}
@Observable
boolean isRawOnLine()
{
return _rawOnLine;
}
void setRawOnLine( final boolean rawOnLine )
{
_rawOnLine = rawOnLine;
}
@Action
void updateOnlineStatus()
{
//Updating the observable will force @Memoize method to recalculate
setRawOnLine( getIsOnLine() );
}
private boolean getIsOnLine()
{
return WindowGlobal.navigator().onLine();
}
}
Explicitly causing Memoized method to re-evaluate
The previous example may have seemed overly complex as and inefficient as we listen to changes from the browser
and reflect the state of the browser in an observable RawOnLine
just so a memoized method will be informed that
a dependency has changed and recompute the value to see if OnLine
has changed.
Rather than synthesizing observables just to drive memoized methods it is possible to explicitly trigger a
recalculation of the memoized value. To do this the depType
parameter of the @Memoize
annotation must be set
to AREZ_OR_EXTERNAL
and the application needs to explicitly invoke reportPossiblyChanged()
on the associated
ComputableValue
.
For example, the above example could be rewritten more efficiently as:
@ArezComponent
public abstract class NetworkStatus
{
private final EventListener _listener = e -> updateOnlineStatus();
// Specify depType so can explicitly trigger a recalculation
// of method using reportPossiblyChanged()
@Memoize( depType = DepType.AREZ_OR_EXTERNAL )
public boolean isOnLine()
{
return WindowGlobal.navigator().onLine();
}
@ComputableValueRef
abstract ComputableValue<Boolean> getOnLineComputableValue();
@OnActivate
void onOnLineActivate()
{
WindowGlobal.addOnlineListener( _listener );
WindowGlobal.addOfflineListener( _listener );
}
@OnDeactivate
void onOnLineDeactivate()
{
WindowGlobal.removeOnlineListener( _listener );
WindowGlobal.removeOfflineListener( _listener );
}
@Action
void updateOnlineStatus()
{
// Explicitly trigger a recalculation of the OnLine value
getOnLineComputableValue().reportPossiblyChanged();
}
}