code: public Closure<V> memoizeAtLeast(final int protectedCacheSize) {
          if (protectedCacheSize < 0) throw new IllegalArgumentException("A non-negative number is required as the protectedCacheSize parameter for memoizeAtLeast.");
  
          return Memoize.buildSoftReferenceMemoizeFunction(protectedCacheSize, new UnlimitedConcurrentCache(), this);
      }
docs: Creates a caching variant of the closure with automatic cache size adjustment and lower limit
  on the cache size.
  Whenever the closure is called, the mapping between the parameters and the return value is preserved in cache
  making subsequent calls with the same arguments fast.
  This variant allows the garbage collector to release entries from the cache and at the same time allows
  the user to specify how many entries should be protected from the eventual gc-initiated eviction.
  Cached entries exceeding the specified preservation threshold are made available for eviction based on
  the LRU (Last Recently Used) strategy.
  Given the non-deterministic nature of garbage collector, the actual cache size may grow well beyond the limits
  set by the user if memory is plentiful.
  The returned function can be safely used concurrently from multiple threads, however, the implementation
  values high average-scenario performance and so concurrent calls on the memoized function with identical argument values
  may not necessarily be able to benefit from each other's cached return value. Also the protectedCacheSize parameter
  might not be respected accurately in such scenarios for some periods of time. With this having been mentioned,
  the performance trade-off still makes concurrent use of memoized functions safe and highly recommended.
  
  The cache gets garbage-collected together with the memoized closure.
  @param protectedCacheSize Number of cached return values to protect from garbage collection
  @return A new function forwarding to the original one while caching the results