crystal-lang

The struct does not update the getter if it is nested in another struct


Here is the code that reproduces the problem - https://carc.in/#/r/f4dp

module CounterTree
  struct Counter
    getter table : Hash(UInt64, UInt64)
    getter total : UInt64

    def initialize
      @table = Hash(UInt64, UInt64).new
      @total = 0_u64
    end

    def increment : UInt64
      insert(key = timestamp, value : UInt64 = 1)
    end

    def insert(key : UInt64, value : UInt64) : UInt64
      if @table.has_key?(key)
        @table[key] += value
      else
        @table[key] = value
      end
      increment_total(value)
      value
    end

    private def increment_total(value : UInt64) : UInt64
      @total += value
    end

    private def timestamp
      Time.local.to_unix_ms.to_u64
    end
  end

  struct Tree
    getter branches = [] of Hash(UInt64, CounterTree::Counter)
    getter branches_num : UInt32

    def initialize(@branches_num = 9)
      @branches = Array.new(@branches_num) do
        Hash(UInt64, CounterTree::Counter).new
      end
    end

    def increment(key : UInt64) : UInt64
      with_counter(key) do
        increment
      end
    end

    private def with_counter(key : UInt64)
      index = (key % @branches_num).to_u64
      unless @branches[index][key]?
        @branches[index][key] = Counter.new
      end
      with @branches[index][key] yield
    end
  end
end

# Total is updated
counter = CounterTree::Counter.new

counter.increment
counter.increment
counter.increment

pp counter

puts "---------"

# Total is not updated
tree = CounterTree::Tree.new

tree.increment 1234_u64
tree.increment 1234_u64
tree.increment 1234_u64

pp tree

I have a value counter that is a simple hash table wrapped in a Counter struct. I increment the getter value total each time it is updated for caching purposes.

I also have a Tree struct, which I need to manage groups of counters. It's arranged in a similar way - inside a hash table, where the key is some numeric value, and the value of the key is Counter.

The problem is that if I create a Counter instance and increment the value, the getter total is updated correctly, but if I do it through Tree, the getter is always zero.

At first I thought there was some problem with the yield and block inline, but trying to rewrite without yield gave exactly the same result.

Where should I look for the problem?


Solution

  • The problem is solved by using class instead of struct because:

    Mutable structs are still allowed, but you should be careful when writing code involving mutability if you want to avoid surprises that are described below.