Hi Cole, Ken,
Thanks both, enough to start building, and the implementation turned up a
few things worth bringing back before I go further.
A correction to my own proposal first: P.Within[string](...) isn't
valid Go, methods can't take type parameters, only the receiver can,
so a typed P
would have to be free functions. I've set the P rework aside and focused on
the step signatures, where most of the value is.
Phase 1 went in cleanly. For steps whose contract is a single primitive with
no Scope/GValue/Traversal/predicate overload, the concrete type works
directly:
As/Cap/Loops/Profile/Tree/Group/GroupCount(...string),
Math/Format/Subgraph(string), Times(int)
Go's untyped constants assign straight in, so bare literals still work,
As("a"), Times(2), no wrappers, no call-site churn, mistyped args rejected
at compile time. I left anything with a Scope (Aggregate, Limit, Tail),
GValue (out/in/both, coin) or Traversal (concat) overload alone, since typing
those would reject queries that are valid today.
The AddV trial Cole suggested is where it gets interesting. Typed arguments
collide with two things at once: the GValue<String> overload, and parameter
bindings (the Gherkin harness passes bound params as untyped values). The
only Go construct that resolves both is the accepted-type interface (option
c), with GValue as a member:
type VertexLabelArg interface { ... } // VLabel(string) |
*Traversal | GValue
g.AddV(VLabel("person"))
g.AddV(__.constant("software"))
That compiles and passes end to end, but it forces a trade-off I'd like the
list to weigh in on before I do the other ~20 value-bearing steps:
Option 1 -- accepted-type interface everywhere. Full compile-time safety,
one method per step; cost is users wrap literals (g.AddV(VLabel("person"))
vs g.AddV("person")), plus a small per-step wrapping rule in the generator.
Option 2 -- keep `any` + runtime validation on the polymorphic positions.
Keeps g.AddV("person") as today, but no compile-time safety where the
values are.
The narrow steps are clean either way; this only bites the polymorphic ones,
and it changes how every gremlin-go user writes them. My lean is option 1 for
predicate/traversal-shaped args and `any` where the arg is almost always a
plain literal, but I'd rather hear the project's preference than guess.
On extending this to Python/TS: you're right, Ken, I'll drop that framing,
it was just an early idea. Go has the most restrictive generics of the three,
so it's a poor template to push outward; TS and Python can follow Java/C#
directly. I'd scope this strictly to gremlin-go. (Agreed too that python
having no type hints at all is the bigger pain, probably its own thread.)
Timing: clean replacement in 4.x for the narrow steps, since we're only
moving errors earlier; the polymorphic steps depend on the option above.
Working Phase 1 branch + AddV PoC in hand; I'll open a JIRA + PR once there's
a steer on 1 vs 2.
Thanks,
Rithin