Looking at the syntax, I wonder how close one could get to it in a "normal" programming language.
x -> y: hello world
declares a connection between two
shapes, x and y, with the label,
hello world
When I read this, I see this Python line in my mind:
connect('x', 'y', 'hello world')
This is of course a lot longer. The reason is that D2 seems to rather use operators than functions. So another approach could be
'x' * 'y' + 'hello world'
This would be possible to implement if the language supports overloading the __add__ and __mul__ functions of the str class. Python does not support it though. So I guess one would have to put at least one instance of a custom class into the mix. Say 'scene', then one might be able to achieve the above with this line:
scene + 'x' * 'y' + 'hello world'
Meaning "Put a connection between x and y with label 'hello world' into the scene".
Hmm.. also not very close to the D2 syntax. So a DSL for diagrams seems to be warranted.
threeducks
today at 11:23 AM
If you are okay with the edges going from right to left instead of left to right, you can do it like this in Python:
def main():
a, b, c = Node("a"), Node("b"), Node("c")
edges = [
a <- b | "first edge",
b <- c | "second edge",
c <- a | "third edge",
]
print("Edges:")
for edge in edges:
print(edge)
class Node:
def __init__(self, name):
self.name = name
def __lt__(self, other):
return Edge(self, other)
def __neg__(self):
return self
def __or__(self, label):
self.label = label
return self
class Edge:
def __init__(self, a, b):
self.a = a
self.b = b
def __repr__(self):
return f"{self.a.name} <- {self.b.name}: {self.b.label}"
main()
It works like this:
The '-' sign is interpreted as a unary minus, which is simply discarded.
The '<' symbol is handled by the '__lt__' overload of the Node class.
The '|' operator has precedence over '<' in Python, so the edge label is stored in the right-hand side node.
That being said, I will haunt you if you actually use this in production.
In Scala you can do it, because you can define your own operators (which are nothing but method names), and you can extend types you don't control. You are a bit constrained by the operator precedence rules, but it's usually good enough.
It's bad practice to make DSLs left and right, obviously. But when one is warranted, you can.
For example here you could have
"x" --> "y" | "hello world"
dragonwriter
today at 9:31 AM
Not all "normal languages" (or even "dynamic scripting languages") are created equal. While I wouldn't want to aim for something equivalent to the whole d2 syntax as an internal DSL in Ruby, if you wanted to just create edges with optional labels with a syntax where "bare" edges are:
x >> y
and edges with labels are:
x >> y << "hello world"
you can do it like this:
class Diagram
def initialize
@nodes = Hash.new { |h, k| h[k] = Node.new(self, k) }
@edges = {}
end
def node(name)
@nodes[name]
end
def add_edge(edge)
@edges[edge.from_node] ||= {}
@edges[edge.from_node][edge.to_node] = edge
end
def all_edges
@edges.values.flat_map(&:values)
end
def interpret &block
interpreter = D2.new(self)
interpreter.instance_eval(&block)
self
end
def to_s
all_edges.map(&:to_s)
end
def inspect
to_s
end
end
class D2
def initialize(diagram = nil)
@diagram = diagram || Diagram.new
end
def method_missing(name, *args)
@diagram.node(name)
end
end
class Node
def initialize(diagram, name)
@diagram = diagram
@name = name
end
def >>(other_node)
Edge.new(self, other_node).tap do |edge|
@diagram.add_edge(edge)
end
end
def to_s
@name
end
def inspect
"Node(#{to_s})"
end
end
class Edge
def initialize(from_node, to_node, label = nil)
@from_node = from_node
@to_node = to_node
@label = label
end
def <<(label)
@label = label
end
def from_node
@from_node
end
def to_node
@to_node
end
def to_s
"#{@from_node.to_s} -> #{@to_node.to_s}" + (@label ? ":#@label" : "")
end
def inspect
"Edge(#{to_s})"
end
end
And use it like this:
irb(main):090:0> d = Diagram.new
=> []
irb(main):091:1* d.interpret {
irb(main):092:1* x >> y << "hello, world!"
irb(main):093:1* y >> z << "goodbye, cruel world!"
irb(main):094:0> }
=> ["x -> y:hello, world!", "y -> z:goodbye, cruel world!"]
OF course, this only supports a trivial subset of the functionality, and only "renders" it to a text form more like the original d2 syntax. But it does create an object model from the DSL in the Diagram class for which you
could build a renderer.