I think this code sample illustrates the exact opposite. Look how concise it is - you really don't need much code at all to implement a simple interpreter.
I think this code sample illustrates the exact opposite. Look how concise it is - you really don't need much code at all to implement a simple interpreter.
A hosted DSL in a modern high level language gives you the full power of a modern high level language to build the simple constrained language of your choosing, and it can be highly portable to different environments, including keyboard vs network play, and different UI environments.
Because of that, it's much nicer to use the power of the host language directly, so you don't have re-invent all these things everytime - but with free monads you keep the property of having a well defined, business specific list of actions or instructions that can be interpreted in different ways.
That is part of the point, though: An interpreter does not need to be complex. Adding "instructions" that does more complicated things does not need to complicate the interpreter much.
Nor does the implementation language make much difference here - fundamentally the interpreter is only the part up to the definition of SubProg. Everything else is just implementation of specific instructions.
Don't get hung up on the use of assembler here - unless you need blazing speed from the interpreter itself, it's trivial to do in pretty much any language.
Conceptually, the interpreter is basically just a table of instructions, a bytecode buffer, and a loop that keeps fetching the next instruction, looks it up in the table, and calls it. The "SubProg" instruction could reasonable be considered part of the generic aspect of the interpreter, but the rest are the domain specific bits, no different
You can abstract out the instructions easily enough. E.g. here's a trivially dumb interpreter written in Ruby where the instructions are separate from the interpreter, that works conceptually not much differently to the linked assembler:
class Interpreter
attr_accessor :pc
def initialize program, instructions
@program = program
@instructions = instructions
@pc = 0
end
def getnext
n = @program[@pc]
@pc+=1
return n
end
def run
while i = getnext
@instructions[i].call(self)
end
end
end
# We could make this an array and insist on numeric instructions,
# but that's an implementation detail - the interpreter doesn't even need to know
# if you pass it a hash or an array.
instructions = {
:myarg => ->(interp) { puts "My argument is #{interp.getnext}" },
:helloworld => ->(interp) { puts "Hello World" }
}
program = [:myarg, 42, :helloworld, :myarg, 3]
Interpreter.new(program, instructions).run
Because the program counter is settable, "SubProg" can just as easily be implemented externally to the interpreter, btw.. Adding variables would really just be a matter of exposing another array or hash in the interpreter so that you could add instructions to read/write them.