An experimental (and highly likely useless for real–world) DSL to build a natural–ish DSL language and write your programs using it. Right to the example:
lang = NaturalDSL::Lang.define do
command :route do
keyword :from
token
keyword :to
token
keyword(:takes).with_value
execute do |vm, city1, city2, distance|
distances = vm.read_variable(:distances) || {}
distances[[city1, city2]] = distance
vm.assign_variable(:distances, distances)
end
end
command :how do
keyword :long
keyword :will
keyword :it
keyword :take
keyword :to
keyword :get
keyword :from
token
keyword :to
token
execute do |vm, city1, city2|
distances = vm.read_variable(:distances) || {}
distance = distances[[city1, city2]].value
"Travel from #{city1.name} to #{city2.name} takes #{distance} hours"
end
end
end
result = NaturalDSL::VM.run(lang) do
route from london to glasgow takes 22
route from paris to prague takes 12
how long will it take to get from london to glasgow
end
puts result # => Travel from london to glasgow takes 22 hours
Read more about this experiment in my blog.
Each language consists of commands. Command can contain keywords, tokens and values:
- keyword is something you want to be in the command to be semantically correct, but you don't need to have it to execute the command (e.g.,
to
,from
, etc.); - token is anything that user types, and the typed word will be passed to the execution block;
- value can be read right after the last keyword or token with
with_value
modifier (e.g.,value 42
).
For instance:
keyword token value
↓ ↓ ↓
assign variable a value 1
↑ ↑
command name keyword
Command makes no sense without logic it implements. We can configure it using the execute method: it receives the instance of the current Virtual Machine as well as all tokens and values:
execute do |vm, *args|
# logic goes here
end
This is how we can create a very basic command that remembers values:
command :assign do
keyword :variable
token
keyword(:value).with_value
execute do |vm, token, value|
# how to assign?
end
end
We need to store the data somewhere between commands, and Virtual Machine has that storage, which can be accessed using assign_variable
and read_variable
. Here is the whole definition of language that can store and sum variables:
lang = NaturalDSL::Lang.define do
command :assign do
keyword :variable
token
keyword(:value).with_value
execute { |vm, token, value| vm.assign_variable(token, value) }
end
command :sum do
token
keyword :with
token
execute do |vm, left, right|
vm.read_variable(left).value + vm.read_variable(right).value
end
end
end
Finally, we can run the program written in our new DSL using the VM
class:
NaturalDSL::VM.run(lang) do
assign variable a value 1
assign variable b value 2
sum a with b
end
Need to consume the unknown amount of similar primitives? Use zero_or_more
:
lang = NaturalDSL::Lang.define do
command :expose do
token.zero_or_more
execute { |_, *fields| "exposing #{fields.join(', ')}" }
end
end
result = NaturalDSL::VM.run(lang) do
expose id email
end
puts result # => exposing id, email
Sometimes you don't want to see the word value
in your commands. In this case you can rename it by passing an argument:
lang = NaturalDSL::Lang.define do
command :john do
keyword(:takes).with_value
execute { |vm, value| vm.assign_variable(:john, value) }
end
command :jane do
keyword(:takes).with_value
execute { |vm, value| vm.assign_variable(:jane, value) }
end
command :who do
keyword :has
keyword :more
execute do |vm|
name = %i[john jane].max_by { |person| vm.read_variable(person).value }
"#{name} has more apples!"
end
end
end
result = NaturalDSL::VM.run(lang) do
john takes 2
jane takes 3
who has more
end
puts result # => jane has more
Here are a couple of ideas to work on 🙂
Let's allow parts that can be omitted:
lang = NaturalDSL::Lang.define do
command :assign do
keyword(:variable).optional
token
keyword(:value).with_value
execute { |vm, token, value| vm.assign_variable(token, value) }
end
end
result = NaturalDSL::VM.run(lang) do
assign variable a value 1
assign b value 2
end
What if I want to start two commands with the same word? Example:
lang = NaturalDSL::Lang.define do
command :mov do
option do
token.with_value
execute do |vm, register, value|
# do constant assigment
end
end
option do
token
token
execute do |vm, register, register_with_value|
# copy value from register
end
end
end
end
NaturalDSL::VM.run(lang) do
mov a 9
mov a b
end
Add this line to your application's Gemfile, and you're all set:
gem "natural_dsl"
The gem is available as open source under the terms of the MIT License.