Memoization is a technique to improve the efficiency of your code by avoiding repetitive calculations. The idea is to remember past calculations so as not to have to repeat them.
(require racket)
(require racket/base)
When calculating a factorial, it can be helpful to remember past calculations.
(define (factorial n)
(if (= n 1)
1
(* n (factorial (- n 1)))))
(factorial 4)
(factorial 5)
(require racket/trace)
(trace factorial)
(factorial 6)
When we evaluate (factorial 6)
, we need to calculate (factorial 5)
.
When we evaluate (factorial 5)
, we need to calculate (factorial 4)
.
It would save us time if we just remembered the results of the past calculations. We can use a hash table to store those values. For those of you who know python, a hash table is a dict or dictionary. See racket hash tables
Below we define a function memoize
which adds a hash table memory to a given function.
(define ht (make-hash))
ht
(hash-set! ht 'john 23)
(hash-has-key? ht 'john)
(hash-ref ht 'john)
(hash-has-key? ht 'mary)
(hash-ref ht 'mary)
Now we will use a hash table as the memory for memoize
(define (memoize func [table (make-hash)])
(lambda (arg)
(cond ((hash-has-key? table arg)
(begin
(display (list 'used-hash-for arg))
(newline)
(hash-ref table arg)))
(else
(hash-set! table arg (func arg))
(hash-ref table arg)))))
(define factorial (memoize factorial))
(factorial 4)
(factorial 5)
(factorial 6)
We have avoided the repeated calculations!
The Fibonacci sequence is found in math and in nature. Here is a simple recursive function to calculate a Fibonacci number.
(define (fib n)
(if (< n 2)
1
(+ (fib (- n 1)) (fib (- n 2)))))
(fib 4)
(fib 5)
(fib 6)
(trace fib)
(fib 6)
Oof! That's a lot of redundant calculations! Let's memoize that puppy.
(define fib (memoize fib))
(fib 6)
(fib 6)
(fib 8)
That's a big improvement.
The Ackermann function is a famous (or infamous) recursive function.
(define (ack m n)
(cond ((= m 0) (+ n 1))
((= n 0) (ack (- m 1) 1)) ;; m > 0
(else
(ack (- m 1) (ack m (- n 1))))))
(ack 2 1)
(trace ack)
(ack 2 1)
While ack
would benefit from being memoized, we cannot use our prevous memoize
function because ack
, unlike factorial
and fib
, takes two arguments, not one. We need to modify memoize
accordingly.
(define (memoize2 func [table (make-hash)])
(lambda (arg1 arg2)
(cond ((hash-has-key? table (list arg1 arg2))
(begin
(display (list 'used-hash-for arg1 arg2))
(newline)
(hash-ref table (list arg1 arg2))))
(else
(hash-set! table (list arg1 arg2) (func arg1 arg2))
(hash-ref table (list arg1 arg2))))))
(define ack (memoize2 ack))
(ack 2 1)
(ack 2 2)
(ack 3 1)
Without memoization, ack
burns through a boatload of cycles.