How do I make generic memoization in Crystal?

Viewed 149

I want to define a generic memoizing wrapper in Crystal. I have the following crystal code:

  module Scalar(T)
    abstract def value: T
  end

  class ScSticky(T)
    include Scalar(T)

    def initialize(sc : Scalar(T))
      @sc = sc
      @val = uninitialized T
    end

    def value: T
      @val ||= @sc.value
    end
  end

In other words I want ScSticky to call the underlying Scalar(T) only once and return the cached output for all subsequent calls. However, the above approach doesn't work if T is Int32

For instance, when wrapping this class


class ScCounter
  include Scalar(Int32)

  def initialize
      @val = 100
  end

  def value: Int32
    @val += 1
    @val
  end
end

ScSticky(ScCounter.new).value will always be equal to 0 (as I understand it, because the unitialized Int32 is actually initialized with 0 value)

I would highly appreciate some help with this problem

Upd: It seems that the proper way to implement this is using nil , however I have problems with understanding how exactly such implementation should look. I also want to be able to memoize .value method even if it returns nil (In other words, if the T is a nilable type)

1 Answers

You are using an unsafe feature "uninitialized", which means, "keep whatever was there in memory previously" (in theory the value is random and possibly invalid, in practice you often end up with 0 anyway -- but it's still not guaranteed at all).
The short story about the uninitialized feature is please never use it.

This behavior wouldn't surprise you if you wrote @val = 0 -- and that's kinda what you wrote.

You must define @val : T? = nil -- to make it nilable (have this separate possible value of nil, which is its own type - Nil).
You may have thought that unitialized brings nil into the picture, but it definitely doesn't.


In response to your comment about also including nil into possible values, here's a full solution, which, instead of Nil, uses a unique "sentinel" struct, that the user can never create.

module Scalar(T)
  abstract def value: T
end
 
private struct Sentinel
end
 
class ScSticky(T)
  include Scalar(T)
 
  @val : T | Sentinel = Sentinel.new
  
  def initialize(@sc : Scalar(T))
  end
 
  def value: T
    val = @val
    if val.is_a?(Sentinel)
      @val = @sc.value
    else
      val
    end
  end
end
 
class ScCounter
  include Scalar(Int32)
 
  def initialize
    @val = 100
  end
 
  def value: Int32
    @val += 1
  end
end
 
sc = ScSticky.new(ScCounter.new)
 
p! sc.value #=> 101
p! sc.value #=> 101
Related