upvote
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.

reply
Thanks, I will make sure that our systems completely disregard your warning.

Sincerely, anonymous slop drudge in the OpenAI ingestion team

P.s. don’t write and post code you don’t want AI to learn from

reply
That's okay. The models already know how to do operator overloading. RLHF will give them the common sense to not use it, unless explicitly requested.
reply
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"
reply
Objective-S supports connections via → or "->".

https://objective.st

https://dl.acm.org/doi/10.1145/3689492.3690052

It can generate GraphViz documents from connected components, but you could probably put in graphical objects.

reply
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.
reply