Let me share my refactored Intcode with you then and ask further questions!
Usage is:
Intcode(Int64).new("104,1125899906842624,99").run do |output|
p output
end
If the program needs input, you share an array:
input = [] of Int32
Intcode(Int32).new("...", input).run do |output|
case output
when 1 then input << 7
...
end
end
Here’s my current Intcode:
class Intcode(T)
@memory : Array(T)
@ip : T
@rb : T
def initialize(listing : String, @input : Array(T) = Array(T).new)
@memory = listing.split(',').map { |n| T.new(n) }
@ip = zero # instruction pointer
@rb = zero # relative base
end
def run
loop do
case current_opcode
when 1
write 3, read(1) + read(2)
@ip += 4
when 2
write 3, read(1) * read(2)
@ip += 4
when 3
write 1, @input.shift
@ip += 2
when 4
yield read(1)
@ip += 2
when 5
@ip = read(1) != 0 ? read(2) : @ip + three
when 6
@ip = read(1) == 0 ? read(2) : @ip + three
when 7
write 3, read(1) < read(2) ? one : zero
@ip += 4
when 8
write 3, read(1) == read(2) ? one : zero
@ip += 4
when 9
@rb += read(1)
@ip += 2
when 99
break
end
end
end
private def ensure_enough_memory(offset)
address = address(offset)
if address >= @memory.size
extension = Array(T).new(address - @memory.size + 1, zero)
@memory.concat(extension)
end
yield address
end
private def write(offset, value)
ensure_enough_memory(offset) do |address|
@memory[address] = value
end
end
private def read(offset)
ensure_enough_memory(offset) do |address|
@memory[address]
end
end
private def current_instruction
@memory[@ip]
end
private def current_opcode
current_instruction % 100
end
private def current_modes
current_instruction // 100 #/
end
private def mode(offset)
modes = current_modes
(offset - 1).times { modes //= 10 } #/
modes % 10
end
private def address(offset)
case mode(offset)
when 0 then @memory[@ip + offset] # position mode
when 1 then @ip + offset # immediate mode
when 2 then @rb + @memory[@ip + offset] # relative mode
else raise "invalid mode"
end
end
@[AlwaysInline]
private def zero
T.new(0)
end
@[AlwaysInline]
private def one
T.new(1)
end
@[AlwaysInline]
private def three
T.new(3)
end
end
Questions:
Does it look idiomatic?
I wanted to define constants like ZERO = T.new(0), but the compiler complained (undefined constant T), that’s why the inlined methods. Is there a better way to accomplish this?
Regarding the generic example, which are the valid places where you can refer to T?
I mistakenly thought of generics as pure templates, but I see require does execute the class-level code. So, for example, you can conditionally execute code (not method definitions), define constants, etc, and at that point no concrete type is being defined, so I understand the error I got.
I guess you can use T within class or instance methods, and top-level declaration of ivars types. Are there other valid places?
Ah, I know what made me think that generics had to be templates.
Coming from Ruby, I expect a top-level self to be a class object. In Crystal, I guess the analogous concept is that self is a type (?). So, I thought of generic types as useless per se, only used to generate concrete types.
But self in the body class of a generic type evaluates to the “abstract” generic type itself. That surprises me and I suspect it is telling me I do not fully understand the fine details of generics.