Skip to content

Commit 617d928

Browse files
authored
Add a deep collection comparison to memoizer in checked mode (#1575)
* Add a deep collection comparison to memoizer in checked mode * Fix tests and extract to a separate class * Comment update * Review comments
1 parent 75c5573 commit 617d928

File tree

3 files changed

+71
-20
lines changed

3 files changed

+71
-20
lines changed

lib/src/model_utils.dart

Lines changed: 50 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import 'package:analyzer/dart/element/element.dart';
1212
import 'package:analyzer/src/generated/engine.dart';
1313
import 'package:analyzer/src/generated/sdk.dart';
1414
import 'package:analyzer/src/generated/source_io.dart';
15+
import 'package:collection/collection.dart';
1516
import 'package:dartdoc/src/model.dart';
1617
import 'package:quiver_hashcode/hashcode.dart';
1718

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

194-
/// Extend or use as a mixin to track object-specific cached values, or
195-
/// instantiate directly to track other values.
195+
/// Like [Memoizer], except in checked mode will validate that the value of the
196+
/// memoized function is unchanging using [DeepCollectionEquality]. Still
197+
/// returns the cached value assuming the assertion passes.
198+
class ValidatingMemoizer extends Memoizer {
199+
bool _assert_on_difference = false;
200+
201+
ValidatingMemoizer() : super() {
202+
// Assignment within assert to take advantage of the expression only
203+
// being executed in checked mode.
204+
assert(_assert_on_difference = true);
205+
invalidateMemos();
206+
}
207+
208+
/// In checked mode and when constructed with assert_on_difference == true,
209+
/// validate that the return value from f() equals the memoized value.
210+
/// Otherwise, a wrapper around putIfAbsent.
211+
@override
212+
R _cacheIfAbsent<R>(_HashableList key, R Function() f) {
213+
if (_assert_on_difference) {
214+
if (_memoizationTable.containsKey(key)) {
215+
R value = f();
216+
if (!new DeepCollectionEquality()
217+
.equals(value, _memoizationTable[key])) {
218+
throw new AssertionError('${value} != $_memoizationTable[key]');
219+
}
220+
}
221+
}
222+
return super._cacheIfAbsent(key, f);
223+
}
224+
}
225+
226+
/// A basic Memoizer class. Instantiate as a member variable, extend, or use
227+
/// as a mixin to track object-specific cached values, or instantiate directly
228+
/// to track other values.
196229
///
197230
/// For all methods in this class, the parameter [f] must be a tear-off method
198231
/// or top level function (not an inline closure) for memoization to work.
@@ -219,44 +252,45 @@ class _HashableList extends UnmodifiableListView<dynamic> {
219252
/// ```
220253
class Memoizer {
221254
/// Map of a function and its positional parameters (if any), to a value.
222-
Map<_HashableList, dynamic> _memoizationTable;
223-
224-
Memoizer() {
225-
invalidateMemos();
226-
}
255+
Map<_HashableList, dynamic> _memoizationTable = new Map();
227256

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

262+
/// A wrapper around putIfAbsent, exposed to allow overrides.
263+
R _cacheIfAbsent<R>(_HashableList key, R Function() f) {
264+
return _memoizationTable.putIfAbsent(key, f);
265+
}
266+
233267
/// Calls and caches the return value of [f]() if not in the cache, then
234268
/// returns the cached value of [f]().
235269
R memoized<R>(Function f) {
236270
_HashableList key = new _HashableList([f]);
237-
return _memoizationTable.putIfAbsent(key, f);
271+
return _cacheIfAbsent(key, f);
238272
}
239273

240274
/// Calls and caches the return value of [f]([param1]) if not in the cache, then
241275
/// returns the cached value of [f]([param1]).
242276
R memoized1<R, A>(R Function(A) f, A param1) {
243277
_HashableList key = new _HashableList([f, param1]);
244-
return _memoizationTable.putIfAbsent(key, () => f(param1));
278+
return _cacheIfAbsent(key, () => f(param1));
245279
}
246280

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

254288
/// Calls and caches the return value of [f]([param1], [param2], [param3]) if
255289
/// not in the cache, then returns the cached value of [f]([param1],
256290
/// [param2], [param3]).
257291
R memoized3<R, A, B, C>(R Function(A, B, C) f, A param1, B param2, C param3) {
258292
_HashableList key = new _HashableList([f, param1, param2, param3]);
259-
return _memoizationTable.putIfAbsent(key, () => f(param1, param2, param3));
293+
return _cacheIfAbsent(key, () => f(param1, param2, param3));
260294
}
261295

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

272305
/// Calls and caches the return value of [f]([param1], [param2], [param3],
@@ -276,8 +309,7 @@ class Memoizer {
276309
C param3, D param4, E param5) {
277310
_HashableList key =
278311
new _HashableList([f, param1, param2, param3, param4, param5]);
279-
return _memoizationTable.putIfAbsent(
280-
key, () => f(param1, param2, param3, param4, param5));
312+
return _cacheIfAbsent(key, () => f(param1, param2, param3, param4, param5));
281313
}
282314

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

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

@@ -319,7 +351,7 @@ class Memoizer {
319351
H param8) {
320352
_HashableList key = new _HashableList(
321353
[f, param1, param2, param3, param4, param5, param6, param7, param8]);
322-
return _memoizationTable.putIfAbsent(
354+
return _cacheIfAbsent(
323355
key,
324356
() =>
325357
f(param1, param2, param3, param4, param5, param6, param7, param8));

pubspec.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -408,4 +408,4 @@ packages:
408408
source: hosted
409409
version: "2.1.13"
410410
sdks:
411-
dart: ">=1.23.0 <=2.0.0-edge.77d3b702203a852723dd2c966d4c0889986a4703"
411+
dart: ">=1.23.0 <=2.0.0-dev.15.0"

test/model_utils_test.dart

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,17 @@ library dartdoc.model_utils_test;
77
import 'package:dartdoc/src/model_utils.dart';
88
import 'package:test/test.dart';
99

10+
class ValidatingMemoizerUser extends ValidatingMemoizer {
11+
int foo = 0;
12+
// These are actually not things you would ordinarily memoize, because
13+
// they change. But they are useful for testing.
14+
List<int> _toMemoize() {
15+
return [foo++];
16+
}
17+
18+
List<int> get toMemoize => memoized(_toMemoize);
19+
}
20+
1021
class MemoizerUser extends Memoizer {
1122
int foo = 0;
1223
// These are actually not things you would ordinarily memoize, because
@@ -130,7 +141,15 @@ void main() {
130141
});
131142
});
132143

133-
group('model_utils MethodMemoizer', () {
144+
group('model_utils ValidatingMemoizer', () {
145+
test('assert on changing underlying function', () {
146+
var m = new ValidatingMemoizerUser();
147+
expect(m.toMemoize.first, equals(0));
148+
expect(() => m.toMemoize, throwsA(new isInstanceOf<AssertionError>()));
149+
});
150+
});
151+
152+
group('model_utils Memoizer', () {
134153
test('basic memoization and invalidation', () {
135154
var m = new MemoizerUser();
136155
expect(m.toMemoize, equals(0), reason: "initialization problem");

0 commit comments

Comments
 (0)