diff --git a/lib/dfunc.dart b/lib/dfunc.dart index 8973813..c75d9d5 100644 --- a/lib/dfunc.dart +++ b/lib/dfunc.dart @@ -23,6 +23,7 @@ export 'src/map.dart'; export 'src/map_by.dart'; export 'src/map_indexed.dart'; export 'src/maybe.dart'; +export 'src/memoize.dart'; export 'src/optional.dart'; export 'src/pipe.dart'; export 'src/scope.dart'; diff --git a/lib/src/memoize.dart b/lib/src/memoize.dart new file mode 100644 index 0000000..52bf71f --- /dev/null +++ b/lib/src/memoize.dart @@ -0,0 +1,47 @@ +/// {@template dfunc.memoize} +/// The `memoize` function is used to cache the results of expensive function +/// calls and return the cached result when the same inputs are provided again. +/// +/// Usage: +/// +/// ```dart +/// final memoized = memoize(expensiveFunction); +/// final result = memoized(arg); +/// ``` +/// {@endtemplate} +Memoize memoize( + A Function(B lastArgument) f, { + int? capacity, +}) => + Memoize(f, capacity: capacity); + +/// {@macro dfunc.memoize} +class Memoize { + /// {@macro dfunc.memoize} + Memoize(this._f, {int? capacity}) : _capacity = capacity; + + final Map _cache = {}; + + final A Function(B lastArgument) _f; + final int? _capacity; + + /// Returns the cached value for the argument [b] if it exists, otherwise + /// computes the value using the function [_f] and caches it. + A call(B b) { + if (_cache.containsKey(b)) return _cache[b] as A; + + final value = _f(b); + _cache[b] = value; + + // ignore: avoid-non-null-assertion, checked for null + if (_capacity != null && _cache.length > _capacity!) { + _cache.remove(_cache.keys.first); + } + + return value; + } + + /// Clears the memoization cache, subsequent call with the same argument will + /// recompute the value. + void reset() => _cache.clear(); +} diff --git a/pubspec.yaml b/pubspec.yaml index a7d613f..f6c7fc1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -8,5 +8,5 @@ environment: dev_dependencies: melos: ^3.0.1 - mews_pedantic: ^0.23.0 + mews_pedantic: ^0.26.0 test: ^1.16.2 diff --git a/test/intersperse_test.dart b/test/intersperse_test.dart index f7d0c0c..c55cea8 100644 --- a/test/intersperse_test.dart +++ b/test/intersperse_test.dart @@ -1,3 +1,5 @@ +// ignore_for_file: avoid-unnecessary-collections, avoid-duplicate-collection-elements + import 'package:dfunc/dfunc.dart'; import 'package:test/test.dart'; diff --git a/test/memoize_test.dart b/test/memoize_test.dart new file mode 100644 index 0000000..f250f01 --- /dev/null +++ b/test/memoize_test.dart @@ -0,0 +1,81 @@ +// ignore_for_file: avoid-duplicate-test-assertions, avoid-duplicate-collection-elements + +import 'package:dfunc/dfunc.dart'; +import 'package:test/test.dart'; + +void main() { + test('memoize', () { + int counter = 0; + + final memoized = memoize((int arg) { + counter++; + + return arg * 2; + }); + + expect(memoized(1), 2); + expect(counter, 1); + + // Doesn't call the function again. + expect(memoized(1), 2); + expect(counter, 1); + + // Calls the function for a different argument. + expect(memoized(2), 4); + expect(counter, 2); + + // Doesn't call the function again. + expect(memoized(1), 2); + expect(counter, 2); + }); + + test('memoize with limited capacity', () { + int counter = 0; + + final memoized = memoize(capacity: 1, (int arg) { + counter++; + + return arg * 2; + }); + + expect(memoized(1), 2); + expect(counter, 1); + + // Doesn't call the function again. + expect(memoized(1), 2); + expect(counter, 1); + + // Calls the function for a different argument. + expect(memoized(2), 4); + expect(counter, 2); + + // Calls the function again if capacity exceeded. + expect(memoized(1), 2); + expect(counter, 3); + }); + + test('memoize async', () async { + int counter = 0; + + final memoized = memoize((int arg) async { + await Future.delayed(Duration.zero); + counter++; + + return arg * 2; + }); + + expect(await memoized(1), 2); + expect(counter, 1); + + // Doesn't call the function again. + expect(await memoized(1), 2); + expect(counter, 1); + + // Calls the function one time for a different argument. + expect( + await Future.wait([memoized(2), memoized(2), memoized(2)]), + [4, 4, 4], + ); + expect(counter, 2); + }); +}