You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@tinkerpop.apache.org by Rithin Kumar <ri...@gmail.com> on 2026/06/01 06:35:40 UTC

[DISCUSS] Typed step arguments in gremlin-go traversal API

Hi all,

I'd like to propose typing the step argument signatures across the
gremlin-go public traversal API, replacing the dominant
`...interface{}` parameters with proper Go types. Targets the 4.x
line. This grew out of an earlier informal conversation with Cole,
whose framing was that gremlin-go would be the first GLV typed,
with Python type hints and TS gremlin-javascript to follow once
Go validates the pattern. Posting here to widen the discussion
before any JIRA or PR.

Problem
=======

Today gremlin-go's user-facing traversal API uses
`(args ...interface{})` for nearly every step (137 of 138 methods
in graphTraversal.go, with mirrors on AnonymousTraversal and
graphTraversalSource -- ~500 interface{} sites total).
Consequences:

  * No compile-time arity or type checking. Calls like
    `g.V().HasLabel(P.Within("a", 1, true))` build and ship; the
    server rejects them at parse time.
  * gopls (the Go LSP) cannot offer per-arg autocomplete or type-
    check at call sites, because every parameter accepts anything.
  * Existing typed constructs (Direction, T, Order, Scope,
    Cardinality named types; TraversalStrategy; *Vertex/*Edge) are
    erased the instant they enter `(args ...interface{})`.
  * `P` is currently a non-generic interface backed by
    `[]interface{}`, so predicate construction itself loses type
    safety.

Gremlin.Net solved the equivalent problem in TINKERPOP-1752 (2017,
Fixed) using C# method overloads. Go has no method overloads, no
sum types, and no method-level type parameters independent of the
receiver -- so the .NET strategy doesn't transfer directly.

Constraints from Go that shape any solution
===========================================

  * No method overloads.
  * Method type parameters must come from the receiver. A generic
    `(g *GraphTraversal[V]) Has(key string, value V)` would require
    GraphTraversal itself to be generic, i.e. the Traversal<S,E>
    pattern. That is explicitly out of scope for this proposal.
  * No sum / union types for parameter positions.

So generics in this proposal apply to the value types we pass
around (`P`, `TextP`), not to GraphTraversal's step methods.

Proposed Phase 1 (small vertical, ships first)
==============================================

A narrow slice that proves the approach:

  * Make `P` generic over its value type: `P[V any]`, with
    factories `P.Eq[V](v V)`, `P.Within[V](vs ...V)`, etc.,
    mirroring Java's `P<V>`. Catches mixed-type predicates at
    compile time today.

    Before:
        g.V().HasLabel(P.Within("a", 1, true))   // compiles, fails on server
    After (illustrative):
        g.V().HasLabel(P.Within[string]("a", "b", "c"))  // ok
        g.V().HasLabel(P.Within[string]("a", 1, true))   // does not compile

  * `TextP[V]` follows the same shape (it is a P clone).
  * Type the obvious-narrow steps:
        string-variadic: As, HasLabel, HasKey, Cap, Aggregate, Select
        int64:           Limit, Range, Skip, Tail, Times
  * Leave Has, By, Where, Property, AddV, AddE, From, To and the
    other polymorphic-arg steps untouched in Phase 1.

Phase 2+ (subject to question 1 below)
======================================

For the polymorphic-arg steps, the realistic Go shapes are:

  * Narrow variadics where overloads collapse cleanly
    (e.g. `From(label string)` + `FromTraversal(*GraphTraversal)`).
  * Named variants for irreducible Java overloads
    (e.g. `Has(key, value any)` + `HasPredicate(key, p Predicate)`).
  * Keep `any` for genuinely user-supplied values (Property values,
    Has values).

I considered and rejected a "one Go method per Java overload with
a unique name" model -- it would balloon the API from ~50 step
methods to 200+ with no precedent in any other GLV.

Questions where I'd value the dev list's input
==============================================

1. Overload-collapse strategy for the polymorphic-arg steps.
   Several Java methods have many semantically distinct overloads
   (Has=9, By=10, AddV=4, Limit=4, Where=3). Because Go has no
   overloads, no sum types, and no method-level type parameters
   independent of the receiver, each Java overload group has to
   collapse one of three ways:

     a. Multiple named Go methods (HasPredicate, HasInLabel, ...).
        Maximal compile-time safety, largest API surface.
     b. Single signature with `any` + runtime validation. Smallest
        API surface; preserves today's call shape with light
        additional safety.
     c. Sum-style accepted-type interfaces
        (`type HasValue interface { hasValue() }` implemented by
        the accepted concrete types). Compile-time safety without
        API balloon, but users wrap primitives or rely on
        conversion helpers.

   gremlin-go would be setting the template here; Python (with
   @overload + Union) and TS gremlin-javascript (with overload
   signatures + unions) would express the equivalent pattern
   later. The conceptual choice -- split vs collapse vs constrain
   -- propagates across all three.

2. Timing: 4.x deprecate-alongside vs 5.0 hard-replace? Cole's
   earlier guidance was "wanted for TP4 but not a release
   blocker." Inside 4.x, project policy
   (for-committers.asciidoc:237-256) requires deprecate-then-
   remove. Because Go cannot have two methods with the same name
   and different signatures, deprecate-alongside means coining
   new method names (e.g. HasLabels(...string) alongside the
   deprecated HasLabel(args ...interface{})) -- ergonomically
   awkward, with the rename-back happening at 5.0. The alternative
   is hard-replace targeted at 5.0 (cleanest but later). Which
   path does the project prefer?

Prior art: TINKERPOP-1752 (Gremlin.Net type-safe methods, Fixed
2017). Will open a TINKERPOP JIRA once the design has rough
consensus here.

Thoughts? Especially curious to hear Cole and Ken given the
earlier informal chats, and anyone with strong opinions on Go API
ergonomics.

Thanks,
Rithin

Re: [DISCUSS] Typed step arguments in gremlin-go traversal API

Posted by Rithin Kumar <ri...@gmail.com>.
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

Re: [DISCUSS] Typed step arguments in gremlin-go traversal API

Posted by Cole Greer <co...@apache.org>.
Hi Rithin,

Apologies for my delayed response. I think the scope you defined for phase 1 is
concise, clear, and non-controversial. All of those cases are simply moving an
error from runtime in the server, to compile-time in the client. For all steps in which
the new argument types are not fundamentally restricting the usage of a step, just
giving faster and more useful error, I agree with implementing it now for
TP4, as a complete replacement of the old `args ...interface{}` method (no need to
deprecate-then-replace).

As for phase 2, I generally agree with Ken's earlier response. I worry that a. would
make gremlin-go too dissimilar from other language variants, and would make it
rather difficult for experienced gremlin users to jump into go. b. has limited upside
but no risk which makes it an ideal floor. c. is intriguing, I'd be interested to see a
PoC of it and/or a list of steps it would benefit. I'm not sure hasValue() is the best
example for it as Java already contains a hasValue(Object, Object...) overload, which
matches fairly well with the current HasValue(args ...interface{}) in go. AddV() would
be an interesting step to trial here. I'm not that familiar with the intricacies of
interface resolution is go, but if we could define an interface which would allow an
argument to be a String, Traversal, or GValue, that would be ideal.

For phase 2, I would try to scope the set of steps to a "best effort" approach. We
should add as much compile time and client side type safety as reasonably possible,
but we shouldn't compromise the language syntax to do so. It is reasonable to fall
back to runtime or server-side type safety checks in places where go makes it
unreasonable to do otherwise. (The Java typing itself is already full of compromises,
there's plenty of room to make Go better even if the end result is less than perfect.)

Regarding your question 2, I touched a bit on it in phase 1. Go definitely makes
deprecate-then-replace difficult. My rule of thumb is that as long as we're just moving
an error forwards and not creating new error cases, then a clean replacement in TP4
is fine. If we're in any way restricting queries which used to be functional, then we
should consider smooth deprecation paths, although a clean break with a clear call
out in the upgrade docs may still be preferable. This would need to be evaluated on a
case-by-case basic. Based on my previous stance on bounding this to a "best-effort"
scope, I would say we're best to avoid any cases which lead to difficult deprecate
or break choices.

Thanks,
Cole

On 2026/06/01 06:35:40 Rithin Kumar wrote:
> Hi all,
> 
> I'd like to propose typing the step argument signatures across the
> gremlin-go public traversal API, replacing the dominant
> `...interface{}` parameters with proper Go types. Targets the 4.x
> line. This grew out of an earlier informal conversation with Cole,
> whose framing was that gremlin-go would be the first GLV typed,
> with Python type hints and TS gremlin-javascript to follow once
> Go validates the pattern. Posting here to widen the discussion
> before any JIRA or PR.
> 
> Problem
> =======
> 
> Today gremlin-go's user-facing traversal API uses
> `(args ...interface{})` for nearly every step (137 of 138 methods
> in graphTraversal.go, with mirrors on AnonymousTraversal and
> graphTraversalSource -- ~500 interface{} sites total).
> Consequences:
> 
>   * No compile-time arity or type checking. Calls like
>     `g.V().HasLabel(P.Within("a", 1, true))` build and ship; the
>     server rejects them at parse time.
>   * gopls (the Go LSP) cannot offer per-arg autocomplete or type-
>     check at call sites, because every parameter accepts anything.
>   * Existing typed constructs (Direction, T, Order, Scope,
>     Cardinality named types; TraversalStrategy; *Vertex/*Edge) are
>     erased the instant they enter `(args ...interface{})`.
>   * `P` is currently a non-generic interface backed by
>     `[]interface{}`, so predicate construction itself loses type
>     safety.
> 
> Gremlin.Net solved the equivalent problem in TINKERPOP-1752 (2017,
> Fixed) using C# method overloads. Go has no method overloads, no
> sum types, and no method-level type parameters independent of the
> receiver -- so the .NET strategy doesn't transfer directly.
> 
> Constraints from Go that shape any solution
> ===========================================
> 
>   * No method overloads.
>   * Method type parameters must come from the receiver. A generic
>     `(g *GraphTraversal[V]) Has(key string, value V)` would require
>     GraphTraversal itself to be generic, i.e. the Traversal<S,E>
>     pattern. That is explicitly out of scope for this proposal.
>   * No sum / union types for parameter positions.
> 
> So generics in this proposal apply to the value types we pass
> around (`P`, `TextP`), not to GraphTraversal's step methods.
> 
> Proposed Phase 1 (small vertical, ships first)
> ==============================================
> 
> A narrow slice that proves the approach:
> 
>   * Make `P` generic over its value type: `P[V any]`, with
>     factories `P.Eq[V](v V)`, `P.Within[V](vs ...V)`, etc.,
>     mirroring Java's `P<V>`. Catches mixed-type predicates at
>     compile time today.
> 
>     Before:
>         g.V().HasLabel(P.Within("a", 1, true))   // compiles, fails on server
>     After (illustrative):
>         g.V().HasLabel(P.Within[string]("a", "b", "c"))  // ok
>         g.V().HasLabel(P.Within[string]("a", 1, true))   // does not compile
> 
>   * `TextP[V]` follows the same shape (it is a P clone).
>   * Type the obvious-narrow steps:
>         string-variadic: As, HasLabel, HasKey, Cap, Aggregate, Select
>         int64:           Limit, Range, Skip, Tail, Times
>   * Leave Has, By, Where, Property, AddV, AddE, From, To and the
>     other polymorphic-arg steps untouched in Phase 1.
> 
> Phase 2+ (subject to question 1 below)
> ======================================
> 
> For the polymorphic-arg steps, the realistic Go shapes are:
> 
>   * Narrow variadics where overloads collapse cleanly
>     (e.g. `From(label string)` + `FromTraversal(*GraphTraversal)`).
>   * Named variants for irreducible Java overloads
>     (e.g. `Has(key, value any)` + `HasPredicate(key, p Predicate)`).
>   * Keep `any` for genuinely user-supplied values (Property values,
>     Has values).
> 
> I considered and rejected a "one Go method per Java overload with
> a unique name" model -- it would balloon the API from ~50 step
> methods to 200+ with no precedent in any other GLV.
> 
> Questions where I'd value the dev list's input
> ==============================================
> 
> 1. Overload-collapse strategy for the polymorphic-arg steps.
>    Several Java methods have many semantically distinct overloads
>    (Has=9, By=10, AddV=4, Limit=4, Where=3). Because Go has no
>    overloads, no sum types, and no method-level type parameters
>    independent of the receiver, each Java overload group has to
>    collapse one of three ways:
> 
>      a. Multiple named Go methods (HasPredicate, HasInLabel, ...).
>         Maximal compile-time safety, largest API surface.
>      b. Single signature with `any` + runtime validation. Smallest
>         API surface; preserves today's call shape with light
>         additional safety.
>      c. Sum-style accepted-type interfaces
>         (`type HasValue interface { hasValue() }` implemented by
>         the accepted concrete types). Compile-time safety without
>         API balloon, but users wrap primitives or rely on
>         conversion helpers.
> 
>    gremlin-go would be setting the template here; Python (with
>    @overload + Union) and TS gremlin-javascript (with overload
>    signatures + unions) would express the equivalent pattern
>    later. The conceptual choice -- split vs collapse vs constrain
>    -- propagates across all three.
> 
> 2. Timing: 4.x deprecate-alongside vs 5.0 hard-replace? Cole's
>    earlier guidance was "wanted for TP4 but not a release
>    blocker." Inside 4.x, project policy
>    (for-committers.asciidoc:237-256) requires deprecate-then-
>    remove. Because Go cannot have two methods with the same name
>    and different signatures, deprecate-alongside means coining
>    new method names (e.g. HasLabels(...string) alongside the
>    deprecated HasLabel(args ...interface{})) -- ergonomically
>    awkward, with the rename-back happening at 5.0. The alternative
>    is hard-replace targeted at 5.0 (cleanest but later). Which
>    path does the project prefer?
> 
> Prior art: TINKERPOP-1752 (Gremlin.Net type-safe methods, Fixed
> 2017). Will open a TINKERPOP JIRA once the design has rough
> consensus here.
> 
> Thoughts? Especially curious to hear Cole and Ken given the
> earlier informal chats, and anyone with strong opinions on Go API
> ergonomics.
> 
> Thanks,
> Rithin
> 

Re: [DISCUSS] Typed step arguments in gremlin-go traversal API

Posted by Ken Hu <ke...@gmail.com>.
Hi Rithin,

I don't really have much of an opinion on gremlin-go per say as it's the
GLV that I know the least. The biggest pain point for me specifically is
actually gremlin-python as that has no type hints whatsoever. I'm
constantly in a position of not knowing what any function expects and what
it returns. While this is true for gremlin-go as well, at least there are
some typed parts of the GLV like the Client API. Also, you mentioned that
what would be done as a part of this proposal would then be extended to
gremlin-python and gremlin-javascript, but that's a bit backwards because
Go has the most restrictive generics. gremlin-javascript, which is
actually TypeScript in 4.x, is the next GLV which could probably just mimic
what is done in Java/C#.

In any case, here are my thoughts to the questions you posed:
1. Suggestion a. honestly just doesn't feel like Gremlin. It makes the
Traversal API feel bloated and hard to use. b. is probably the thing that
feels the most like Gremlin but brings little value, but probably a good
starting point since it doesn't hurt. c. might be the happy medium and
probably the one I'd prototype out first
2. I think the deprecate-then-remove is general guidance. I think it's fine
to just remove in 4.x if consensus is reached on this proposal in the
devlist.

Regards,
Ken

On Sun, May 31, 2026 at 11:37 PM Rithin Kumar <ri...@gmail.com>
wrote:

> Hi all,
>
> I'd like to propose typing the step argument signatures across the
> gremlin-go public traversal API, replacing the dominant
> `...interface{}` parameters with proper Go types. Targets the 4.x
> line. This grew out of an earlier informal conversation with Cole,
> whose framing was that gremlin-go would be the first GLV typed,
> with Python type hints and TS gremlin-javascript to follow once
> Go validates the pattern. Posting here to widen the discussion
> before any JIRA or PR.
>
> Problem
> =======
>
> Today gremlin-go's user-facing traversal API uses
> `(args ...interface{})` for nearly every step (137 of 138 methods
> in graphTraversal.go, with mirrors on AnonymousTraversal and
> graphTraversalSource -- ~500 interface{} sites total).
> Consequences:
>
>   * No compile-time arity or type checking. Calls like
>     `g.V().HasLabel(P.Within("a", 1, true))` build and ship; the
>     server rejects them at parse time.
>   * gopls (the Go LSP) cannot offer per-arg autocomplete or type-
>     check at call sites, because every parameter accepts anything.
>   * Existing typed constructs (Direction, T, Order, Scope,
>     Cardinality named types; TraversalStrategy; *Vertex/*Edge) are
>     erased the instant they enter `(args ...interface{})`.
>   * `P` is currently a non-generic interface backed by
>     `[]interface{}`, so predicate construction itself loses type
>     safety.
>
> Gremlin.Net solved the equivalent problem in TINKERPOP-1752 (2017,
> Fixed) using C# method overloads. Go has no method overloads, no
> sum types, and no method-level type parameters independent of the
> receiver -- so the .NET strategy doesn't transfer directly.
>
> Constraints from Go that shape any solution
> ===========================================
>
>   * No method overloads.
>   * Method type parameters must come from the receiver. A generic
>     `(g *GraphTraversal[V]) Has(key string, value V)` would require
>     GraphTraversal itself to be generic, i.e. the Traversal<S,E>
>     pattern. That is explicitly out of scope for this proposal.
>   * No sum / union types for parameter positions.
>
> So generics in this proposal apply to the value types we pass
> around (`P`, `TextP`), not to GraphTraversal's step methods.
>
> Proposed Phase 1 (small vertical, ships first)
> ==============================================
>
> A narrow slice that proves the approach:
>
>   * Make `P` generic over its value type: `P[V any]`, with
>     factories `P.Eq[V](v V)`, `P.Within[V](vs ...V)`, etc.,
>     mirroring Java's `P<V>`. Catches mixed-type predicates at
>     compile time today.
>
>     Before:
>         g.V().HasLabel(P.Within("a", 1, true))   // compiles, fails on
> server
>     After (illustrative):
>         g.V().HasLabel(P.Within[string]("a", "b", "c"))  // ok
>         g.V().HasLabel(P.Within[string]("a", 1, true))   // does not
> compile
>
>   * `TextP[V]` follows the same shape (it is a P clone).
>   * Type the obvious-narrow steps:
>         string-variadic: As, HasLabel, HasKey, Cap, Aggregate, Select
>         int64:           Limit, Range, Skip, Tail, Times
>   * Leave Has, By, Where, Property, AddV, AddE, From, To and the
>     other polymorphic-arg steps untouched in Phase 1.
>
> Phase 2+ (subject to question 1 below)
> ======================================
>
> For the polymorphic-arg steps, the realistic Go shapes are:
>
>   * Narrow variadics where overloads collapse cleanly
>     (e.g. `From(label string)` + `FromTraversal(*GraphTraversal)`).
>   * Named variants for irreducible Java overloads
>     (e.g. `Has(key, value any)` + `HasPredicate(key, p Predicate)`).
>   * Keep `any` for genuinely user-supplied values (Property values,
>     Has values).
>
> I considered and rejected a "one Go method per Java overload with
> a unique name" model -- it would balloon the API from ~50 step
> methods to 200+ with no precedent in any other GLV.
>
> Questions where I'd value the dev list's input
> ==============================================
>
> 1. Overload-collapse strategy for the polymorphic-arg steps.
>    Several Java methods have many semantically distinct overloads
>    (Has=9, By=10, AddV=4, Limit=4, Where=3). Because Go has no
>    overloads, no sum types, and no method-level type parameters
>    independent of the receiver, each Java overload group has to
>    collapse one of three ways:
>
>      a. Multiple named Go methods (HasPredicate, HasInLabel, ...).
>         Maximal compile-time safety, largest API surface.
>      b. Single signature with `any` + runtime validation. Smallest
>         API surface; preserves today's call shape with light
>         additional safety.
>      c. Sum-style accepted-type interfaces
>         (`type HasValue interface { hasValue() }` implemented by
>         the accepted concrete types). Compile-time safety without
>         API balloon, but users wrap primitives or rely on
>         conversion helpers.
>
>    gremlin-go would be setting the template here; Python (with
>    @overload + Union) and TS gremlin-javascript (with overload
>    signatures + unions) would express the equivalent pattern
>    later. The conceptual choice -- split vs collapse vs constrain
>    -- propagates across all three.
>
> 2. Timing: 4.x deprecate-alongside vs 5.0 hard-replace? Cole's
>    earlier guidance was "wanted for TP4 but not a release
>    blocker." Inside 4.x, project policy
>    (for-committers.asciidoc:237-256) requires deprecate-then-
>    remove. Because Go cannot have two methods with the same name
>    and different signatures, deprecate-alongside means coining
>    new method names (e.g. HasLabels(...string) alongside the
>    deprecated HasLabel(args ...interface{})) -- ergonomically
>    awkward, with the rename-back happening at 5.0. The alternative
>    is hard-replace targeted at 5.0 (cleanest but later). Which
>    path does the project prefer?
>
> Prior art: TINKERPOP-1752 (Gremlin.Net type-safe methods, Fixed
> 2017). Will open a TINKERPOP JIRA once the design has rough
> consensus here.
>
> Thoughts? Especially curious to hear Cole and Ken given the
> earlier informal chats, and anyone with strong opinions on Go API
> ergonomics.
>
> Thanks,
> Rithin
>