Skip to content

Add a deep collection comparison to memoizer in checked mode #1575

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

Merged
merged 5 commits into from
Jan 4, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 50 additions & 18 deletions lib/src/model_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/src/generated/engine.dart';
import 'package:analyzer/src/generated/sdk.dart';
import 'package:analyzer/src/generated/source_io.dart';
import 'package:collection/collection.dart';
import 'package:dartdoc/src/model.dart';
import 'package:quiver_hashcode/hashcode.dart';

Expand Down Expand Up @@ -191,8 +192,40 @@ class _HashableList extends UnmodifiableListView<dynamic> {
get hashCode => hashObjects(this);
}

/// Extend or use as a mixin to track object-specific cached values, or
/// instantiate directly to track other values.
/// Like [Memoizer], except in checked mode will validate that the value of the
/// memoized function is unchanging using [DeepCollectionEquality]. Still
/// returns the cached value assuming the assertion passes.
class ValidatingMemoizer extends Memoizer {
bool _assert_on_difference = false;

ValidatingMemoizer() : super() {
// Assignment within assert to take advantage of the expression only
// being executed in checked mode.
assert(_assert_on_difference = true);
invalidateMemos();
}

/// In checked mode and when constructed with assert_on_difference == true,
/// validate that the return value from f() equals the memoized value.
/// Otherwise, a wrapper around putIfAbsent.
@override
R _cacheIfAbsent<R>(_HashableList key, R Function() f) {
if (_assert_on_difference) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

w/o the init at definition, this will be if (null)

if (_memoizationTable.containsKey(key)) {
R value = f();
if (!new DeepCollectionEquality()
.equals(value, _memoizationTable[key])) {
throw new AssertionError('${value} != $_memoizationTable[key]');
}
}
}
return super._cacheIfAbsent(key, f);
}
}

/// A basic Memoizer class. Instantiate as a member variable, extend, or use
/// as a mixin to track object-specific cached values, or instantiate directly
/// to track other values.
///
/// For all methods in this class, the parameter [f] must be a tear-off method
/// or top level function (not an inline closure) for memoization to work.
Expand All @@ -219,44 +252,45 @@ class _HashableList extends UnmodifiableListView<dynamic> {
/// ```
class Memoizer {
/// Map of a function and its positional parameters (if any), to a value.
Map<_HashableList, dynamic> _memoizationTable;

Memoizer() {
invalidateMemos();
}
Map<_HashableList, dynamic> _memoizationTable = new Map();

/// Reset the memoization table, forcing calls of the underlying functions.
void invalidateMemos() {
_memoizationTable = new Map();
}

/// A wrapper around putIfAbsent, exposed to allow overrides.
R _cacheIfAbsent<R>(_HashableList key, R Function() f) {
return _memoizationTable.putIfAbsent(key, f);
}

/// Calls and caches the return value of [f]() if not in the cache, then
/// returns the cached value of [f]().
R memoized<R>(Function f) {
_HashableList key = new _HashableList([f]);
return _memoizationTable.putIfAbsent(key, f);
return _cacheIfAbsent(key, f);
}

/// Calls and caches the return value of [f]([param1]) if not in the cache, then
/// returns the cached value of [f]([param1]).
R memoized1<R, A>(R Function(A) f, A param1) {
_HashableList key = new _HashableList([f, param1]);
return _memoizationTable.putIfAbsent(key, () => f(param1));
return _cacheIfAbsent(key, () => f(param1));
}

/// Calls and caches the return value of [f]([param1], [param2]) if not in the
/// cache, then returns the cached value of [f]([param1], [param2]).
R memoized2<R, A, B>(R Function(A, B) f, A param1, B param2) {
_HashableList key = new _HashableList([f, param1, param2]);
return _memoizationTable.putIfAbsent(key, () => f(param1, param2));
return _cacheIfAbsent(key, () => f(param1, param2));
}

/// Calls and caches the return value of [f]([param1], [param2], [param3]) if
/// not in the cache, then returns the cached value of [f]([param1],
/// [param2], [param3]).
R memoized3<R, A, B, C>(R Function(A, B, C) f, A param1, B param2, C param3) {
_HashableList key = new _HashableList([f, param1, param2, param3]);
return _memoizationTable.putIfAbsent(key, () => f(param1, param2, param3));
return _cacheIfAbsent(key, () => f(param1, param2, param3));
}

/// Calls and caches the return value of [f]([param1], [param2], [param3],
Expand All @@ -265,8 +299,7 @@ class Memoizer {
R memoized4<R, A, B, C, D>(
R Function(A, B, C, D) f, A param1, B param2, C param3, D param4) {
_HashableList key = new _HashableList([f, param1, param2, param3, param4]);
return _memoizationTable.putIfAbsent(
key, () => f(param1, param2, param3, param4));
return _cacheIfAbsent(key, () => f(param1, param2, param3, param4));
}

/// Calls and caches the return value of [f]([param1], [param2], [param3],
Expand All @@ -276,8 +309,7 @@ class Memoizer {
C param3, D param4, E param5) {
_HashableList key =
new _HashableList([f, param1, param2, param3, param4, param5]);
return _memoizationTable.putIfAbsent(
key, () => f(param1, param2, param3, param4, param5));
return _cacheIfAbsent(key, () => f(param1, param2, param3, param4, param5));
}

/// Calls and caches the return value of [f]([param1], [param2], [param3],
Expand All @@ -287,7 +319,7 @@ class Memoizer {
B param2, C param3, D param4, E param5, F param6) {
_HashableList key =
new _HashableList([f, param1, param2, param3, param4, param5, param6]);
return _memoizationTable.putIfAbsent(
return _cacheIfAbsent(
key, () => f(param1, param2, param3, param4, param5, param6));
}

Expand All @@ -299,7 +331,7 @@ class Memoizer {
A param1, B param2, C param3, D param4, E param5, F param6, G param7) {
_HashableList key = new _HashableList(
[f, param1, param2, param3, param4, param5, param6, param7]);
return _memoizationTable.putIfAbsent(
return _cacheIfAbsent(
key, () => f(param1, param2, param3, param4, param5, param6, param7));
}

Expand All @@ -319,7 +351,7 @@ class Memoizer {
H param8) {
_HashableList key = new _HashableList(
[f, param1, param2, param3, param4, param5, param6, param7, param8]);
return _memoizationTable.putIfAbsent(
return _cacheIfAbsent(
key,
() =>
f(param1, param2, param3, param4, param5, param6, param7, param8));
Expand Down
2 changes: 1 addition & 1 deletion pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -408,4 +408,4 @@ packages:
source: hosted
version: "2.1.13"
sdks:
dart: ">=1.23.0 <=2.0.0-edge.77d3b702203a852723dd2c966d4c0889986a4703"
dart: ">=1.23.0 <=2.0.0-dev.15.0"
21 changes: 20 additions & 1 deletion test/model_utils_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@ library dartdoc.model_utils_test;
import 'package:dartdoc/src/model_utils.dart';
import 'package:test/test.dart';

class ValidatingMemoizerUser extends ValidatingMemoizer {
int foo = 0;
// These are actually not things you would ordinarily memoize, because
// they change. But they are useful for testing.
List<int> _toMemoize() {
return [foo++];
}

List<int> get toMemoize => memoized(_toMemoize);
}

class MemoizerUser extends Memoizer {
int foo = 0;
// These are actually not things you would ordinarily memoize, because
Expand Down Expand Up @@ -130,7 +141,15 @@ void main() {
});
});

group('model_utils MethodMemoizer', () {
group('model_utils ValidatingMemoizer', () {
test('assert on changing underlying function', () {
var m = new ValidatingMemoizerUser();
expect(m.toMemoize.first, equals(0));
expect(() => m.toMemoize, throwsA(new isInstanceOf<AssertionError>()));
});
});

group('model_utils Memoizer', () {
test('basic memoization and invalidation', () {
var m = new MemoizerUser();
expect(m.toMemoize, equals(0), reason: "initialization problem");
Expand Down