Search code examples
c#genericsmemoization

Can I memoize a Generic Method?


I have 2 expensive Generic methods:

  public T DoStuff<T>()
  {
      //return depends on T.
  }

  public T DoStuffBasedOnString<T>(string input)
  {
      //return depends on T.
  }

Their return values can never vary for a given Type and string.

Is it possible to memoize these functions?


My starting point is a memoize function taken from here: https://www.aleksandar.io/post/memoization/

public static Func<T, TResult> Memoize<T, TResult>(this Func<T, TResult> f)
{
    var cache = new ConcurrentDictionary<T, TResult>();
    return a => cache.GetOrAdd(a, f);
}

Solution

  • I wrote a demo in Linqpad 7 here, it seems to work.

    void Main()
    {
        var doStuff = () => DoStuff<Car>();
        var doStuffMemoized = doStuff.Memoize();
        var stopWatch = Stopwatch.StartNew();
        var car = doStuffMemoized();
        Console.WriteLine($"Retrieving car took:  {stopWatch.ElapsedMilliseconds} ms");
        stopWatch.Reset();
        car = doStuffMemoized(); //second call uses cached result and return almost instantly
        Console.WriteLine($"Retrieving car took:  {stopWatch.ElapsedMilliseconds} ms");
    
        var doStuffBasedOnString = (string arg) => DoStuffBasedOnString<Car>(arg);
        var doStuffBasedOnStringM = doStuffBasedOnString.Memoize(x => x);
        stopWatch = Stopwatch.StartNew();
        var anotherCar = doStuffBasedOnStringM("Volvo 240");
        Console.WriteLine($"Retrieving car took:  {stopWatch.ElapsedMilliseconds} ms");
        stopWatch.Reset();  
        anotherCar = doStuffBasedOnStringM("Volvo 240"); //second call uses cached result and return almost instantly
        Console.WriteLine($"Retrieving car took:  {stopWatch.ElapsedMilliseconds} ms");
    
    }
    
    public class Car {
        public string Model { get; set; }
        public string Make { get; set; }
    }
    
    public T DoStuff<T>() where T : class, new()
    {
        Thread.Sleep(1000);
        var result = Activator.CreateInstance<T>();
        return result;
    }
    
    public T DoStuffBasedOnString<T>(string input)
    {
        Thread.Sleep(2000);
        var result = Activator.CreateInstance<T>();
        return result;
    }
    
    
    public static class FunctionalExtensions
    {
    
        public static Func<TOut> Memoize<TOut>(this Func<TOut> @this)
        {
            var dict = new ConcurrentDictionary<string, TOut>();
            return () =>
            {
                string key = typeof(TOut).FullName;
                if (!dict.ContainsKey(key))
                {
                    dict.TryAdd(key, @this());
                }
                return dict[key];
            };
        }
    
    
    
        public static Func<T1, TOut> Memoize<T1, TOut>(this Func<T1, TOut> @this, Func<T1, string> keyGenerator)
        {
            var dict = new ConcurrentDictionary<string, TOut>();
            return x =>
            {
                string key = keyGenerator(x);
                if (!dict.ContainsKey(key))
                {
                    dict.TryAdd(key, @this(x));
                }
                return dict[key];
            };
        }
    
    }
    

    I have written Memoize methods before, but zero arguments are not usual to do in Memoize. We just cache into the ConcurrentDictionary the full type name instead here. For the 'key generator' in the method that accepts a string argument i just use 'x => x' , this is a method where you tell how to specify the key that goes into the ConcurrentDictionary which governs if we should cache or not.

    Linqpad screenshot showing it works. (I also debugged it a bit)

    Memoize demo - run in Linqpad 7

    Now, note that once you have a memoize method that accepts ONE argument, you probably want to have overloads that also accept multiple arguments, they should be fairly easy to extend.

    I wrote an article for the interested here, it has up to FOUR arguments. There are built in libs out there that already implements Memoize. For example, LanguageExt for C# on GitHub library. Most of the examples I have seen just uses a Dictionary, only use ConcurrentDictionary if you do suspect you will need it for thread safety, sometimes your application only runs in a way that it is not necessary.