Skip to content

Commit df775b0

Browse files
committed
TINKERPOP-3178 choose semantics consistency improvements
Only match on first option for switch semantics. Introduced Pick.unproductive when the choice ends up being unproductive. Pass through unproductive choices for switch semantics so that they are like if-else. Introduce ChooseSemantics enum to inform on whether the step is in if-else or switch form. Lots of documentation improvements.
1 parent 2fee315 commit df775b0

22 files changed

Lines changed: 1187 additions & 78 deletions

File tree

CHANGELOG.asciidoc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,11 @@ This release also includes changes from <<release-3-7-XXX, 3.7.XXX>>.
6060
* Added integer overflow checks.
6161
* Added missing strategies to the `TraversalStrategies` global cache as well as `CoreImports` in `gremlin-groovy`.
6262
* Added missing strategies to `strategies.py` in `gremlin-python`.
63+
* Preferred use of `TokenTraversal` when using `T` with `choose` instead of `LambdaMapTraversal` which treats `T` more abstractly as a `Function`.
64+
* Preferred use of `is()` when using `P` with `choose` instead of a `PredicateTraverser` which allows `P` to be used more concretely rather than as a `Function`.
65+
* Changed `choose` to only use the first `option` matched.
66+
* Added `Pick.unproductive` to allow for matches on unproductive predicates.
67+
* Changed `choose` to pass through traversers of unproductive predicates and unmatched choices.
6368
* Updated `OptionsStrategy` in `gremlin-python` to take options directly as keyword arguments.
6469
* Added static `instance()` method to `ElementIdStrategy` to an instance with the default configuration.
6570
* Updated `ElementIdStrategy.getConfiguration()` to help with serialization.

docs/src/dev/provider/gremlin-semantics.asciidoc

Lines changed: 92 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -827,6 +827,37 @@ Incoming date remains unchanged.
827827
See: link:https://github.com/apache/tinkerpop/tree/x.y.z/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AsDateStep.java[source],
828828
link:https://tinkerpop.apache.org/docs/x.y.z/reference/#asDate-step[reference]
829829
830+
[[asString-step]]
831+
=== asString()
832+
833+
*Description:* Returns the value of incoming traverser as strings, or if `Scope.local` is specified, returns each element inside
834+
incoming list traverser as string.
835+
836+
*Syntax:* `asString()` | `asString(Scope scope)`
837+
838+
[width="100%",options="header"]
839+
|=========================================================
840+
|Start Step |Mid Step |Modulated |Domain |Range
841+
|N |Y |N |`any` |`String`/`List`
842+
|=========================================================
843+
844+
*Arguments:*
845+
846+
* `scope` - Determines the type of traverser it operates on. Both scopes will operate on the level of individual traversers.
847+
The `global` scope will operate on individual traverser, casting all (except `null`) to string. The `local` scope will behave like
848+
`global` for everything except lists, where it will cast individual non-`null` elements inside the list into string and return a
849+
list of string instead.
850+
851+
Null values from the incoming traverser are not processed and remain as null when returned.
852+
853+
*Exceptions*
854+
None
855+
856+
See: link:https://github.com/apache/tinkerpop/tree/x.y.z/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AsStringGlobalStep.java[source],
857+
link:https://github.com/apache/tinkerpop/tree/x.y.z/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AsStringLocalStep.java[source (local)],
858+
link:https://tinkerpop.apache.org/docs/x.y.z/reference/#asString-step[reference]
859+
860+
830861
[[barrier-step]]
831862
=== barrier()
832863
@@ -1030,36 +1061,6 @@ None
10301061
See: link:https://github.com/apache/tinkerpop/tree/x.y.z/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/sideEffect/SideEffectCapStep.java[source],
10311062
link:https://tinkerpop.apache.org/docs/x.y.z/reference/#cap-step[reference]
10321063
1033-
[[asString-step]]
1034-
=== asString()
1035-
1036-
*Description:* Returns the value of incoming traverser as strings, or if `Scope.local` is specified, returns each element inside
1037-
incoming list traverser as string.
1038-
1039-
*Syntax:* `asString()` | `asString(Scope scope)`
1040-
1041-
[width="100%",options="header"]
1042-
|=========================================================
1043-
|Start Step |Mid Step |Modulated |Domain |Range
1044-
|N |Y |N |`any` |`String`/`List`
1045-
|=========================================================
1046-
1047-
*Arguments:*
1048-
1049-
* `scope` - Determines the type of traverser it operates on. Both scopes will operate on the level of individual traversers.
1050-
The `global` scope will operate on individual traverser, casting all (except `null`) to string. The `local` scope will behave like
1051-
`global` for everything except lists, where it will cast individual non-`null` elements inside the list into string and return a
1052-
list of string instead.
1053-
1054-
Null values from the incoming traverser are not processed and remain as null when returned.
1055-
1056-
*Exceptions*
1057-
None
1058-
1059-
See: link:https://github.com/apache/tinkerpop/tree/x.y.z/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AsStringGlobalStep.java[source],
1060-
link:https://github.com/apache/tinkerpop/tree/x.y.z/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/AsStringLocalStep.java[source (local)],
1061-
link:https://tinkerpop.apache.org/docs/x.y.z/reference/#asString-step[reference]
1062-
10631064
[[call-step]]
10641065
=== call()
10651066
@@ -1145,6 +1146,67 @@ link:https://github.com/apache/tinkerpop/tree/x.y.z/gremlin-core/src/main/java/o
11451146
link:https://github.com/apache/tinkerpop/tree/x.y.z/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/service/ServiceRegistry.java[ServiceRegistry],
11461147
link:https://tinkerpop.apache.org/docs/x.y.z/reference/#call-step[reference]
11471148
1149+
[[choose-step]]
1150+
=== choose()
1151+
1152+
*Description:* A branch step that routes the traverser to different paths based on a choice criterion.
1153+
1154+
*Syntax:* `choose(Traversal|T choice)` | `choose(Traversal|P choice, Traversal trueChoice)` |`choose(Traversal|P choice, Traversal trueChoice, Traversal falseChoice)`
1155+
1156+
[width="100%",options="header"]
1157+
|=========================================================
1158+
|Start Step |Mid Step |Modulated |Domain |Range
1159+
|N |Y |Y |`any` |`any`
1160+
|=========================================================
1161+
1162+
*Arguments:*
1163+
1164+
* `choice` - A `Traversal`, or `T` that produces a value used to determine which option to take. In the `if-then` forms, this value may be a `P` to determine `true` or `false`.
1165+
* `trueChoice` - The traversal to take if the predicate traversal returns a value (has a next element).
1166+
* `falseChoice` - The traversal to take if the predicate traversal returns no value (has no next element).
1167+
1168+
*Modulation:*
1169+
1170+
* `option(pickToken, traversalOption)` - Adds a traversal option to the `choose` step. The `pickToken` is matched
1171+
against the result of the choice traversal. The `pickToken` may be a literal value, a predicate `P`, a `Traversal`
1172+
(whose first returned value is used for matching) or a `Pick` enum value. If a match is found, the traverser is routed
1173+
to the corresponding `traversalOption`.
1174+
1175+
*Considerations:*
1176+
1177+
The `choose()` step is a branch step that routes the traverser to different paths based on a choice criterion. There are
1178+
two main forms of the `choose()` step:
1179+
1180+
1. *if-then form*: `choose(predicate, trueChoice, falseChoice)` - If the predicate traversal or `P` returns a value
1181+
(has a next element), the traverser is routed to the trueChoice traversal. Otherwise, it is routed to the falseChoice
1182+
traversal. If the predicate is unproductive or if the falseChoice is not specified, then the traverser passes through.
1183+
1184+
2. *switch form*: `choose(choice).option(pickValue, resultTraversal)` - The choice which may be a `Traversal` or
1185+
`T` produces a value that is matched against the pickValue of each option. If a match is found, the traverser is routed
1186+
to the corresponding resultTraversal and no further matches are attempted. If no match is found then the traverser
1187+
passes through by default or can be matched on `Pick.none`. If the choiceTraversal is unproductive, then the traverser
1188+
passes through by default or can be matched on `Pick.unproductive`.
1189+
1190+
`choose` does not allow more than one traversal to be assigned to a single `Pick`. The first `Pick` assigned via
1191+
`option` is the one that will be used, similar to how the first pickValue match that is found is used.
1192+
1193+
The `choose()` step ensures that only one option is selected for each traverser, unlike other branch steps like
1194+
`union()` that can route a traverser to multiple paths. As it is like `union()`, note that each `option` stream will
1195+
behave like one:
1196+
1197+
[gremlin-groovy,modern]
1198+
----
1199+
g.V().union(__.has("name", "vadas").values('age').fold(), __.has('name',neq('vadas')).values('name').fold())
1200+
g.V().choose(__.has("name", "vadas"), __.values('age').fold(), __.values('name').fold())
1201+
----
1202+
1203+
*Exceptions*
1204+
1205+
* `IllegalArgumentException` - If `Pick.any` is used as an option token, as only one option per traverser is allowed.
1206+
1207+
See: link:https://github.com/apache/tinkerpop/tree/x.y.z/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/branch/ChooseStep.java[source],
1208+
link:https://tinkerpop.apache.org/docs/x.y.z/reference/#choose-step[reference]
1209+
11481210
[[combine-step]]
11491211
=== combine()
11501212

docs/src/reference/the-traversal.asciidoc

Lines changed: 98 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1077,24 +1077,33 @@ link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gre
10771077
image::choose-step.png[width=700]
10781078
10791079
The `choose()`-step (*branch*) routes the current traverser to a particular traversal branch option. With `choose()`,
1080-
it is possible to implement if/then/else-semantics as well as more complicated selections.
1080+
it is possible to implement two different types of semantics: if-then-else (conditional branching) and switch
1081+
(value-based selection).
1082+
1083+
==== If-Then-Else
1084+
1085+
The if-the-else semantics of `choose()` evaluate a predicate traversal and route the traverser to either the "true"
1086+
branch or the "false" branch based on the result.
10811087
10821088
[gremlin-groovy,modern]
10831089
----
10841090
g.V().hasLabel('person').
10851091
choose(values('age').is(lte(30)),
1086-
__.in(),
1087-
__.out()).values('name') <1>
1092+
__.in(),
1093+
__.out()).values('name') <1>
10881094
g.V().hasLabel('person').
1089-
choose(values('age')).
1090-
option(27, __.in()).
1091-
option(32, __.out()).values('name') <2>
1095+
choose(outE('knows').count().is(gt(0)),
1096+
__.out('knows'),
1097+
__.identity()).values('name') <2>
10921098
----
10931099
1094-
<1> If the traversal yields an element, then do `in`, else do `out` (i.e. true/false-based option selection).
1095-
<2> Use the result of the traversal as a key to the map of traversal options (i.e. value-based option selection).
1100+
<1> If the person's age is less than or equal to 30, then traverse to incoming vertices, else traverse to outgoing
1101+
vertices.
1102+
<2> If the person has outgoing "knows" edges, then traverse to those known vertices, else return the person vertex
1103+
itself.
10961104
1097-
If the "false"-branch is not provided, then if/then-semantics are implemented.
1105+
If the "false"-branch is not provided, then simple if-then-semantics are implemented, where traversers that don't match
1106+
the condition are passed through unchanged.
10981107
10991108
[gremlin-groovy,modern]
11001109
----
@@ -1103,9 +1112,12 @@ g.V().choose(hasLabel('person'), out('created'), identity()).values('name') <2>
11031112
----
11041113
11051114
<1> If the vertex is a person, emit the vertices they created, else emit the vertex.
1106-
<2> If/then/else with an `identity()` on the false-branch is equivalent to if/then with no false-branch.
1115+
<2> if-the-else with an `identity()` on the false-branch is equivalent to if-then with no false-branch.
11071116
1108-
Note that `choose()` can have an arbitrary number of options and moreover, can take an anonymous traversal as its choice function.
1117+
==== Switch
1118+
1119+
The switch semantics of `choose()` use the result of a traversal as a key to select from multiple traversal options.
1120+
This allows for more complex branching logic beyond simple true/false conditions.
11091121
11101122
[gremlin-groovy,modern]
11111123
----
@@ -1114,19 +1126,89 @@ g.V().hasLabel('person').
11141126
option('marko', values('age')).
11151127
option('josh', values('name')).
11161128
option('vadas', elementMap()).
1117-
option('peter', label())
1129+
option('peter', label()) <1>
1130+
g.V().hasLabel('person').
1131+
choose(values('age')).
1132+
option(27, __.in().values('name')).
1133+
option(32, __.out().values('name')) <2>
11181134
----
11191135
1120-
The `choose()`-step can leverage the `Pick.none` option match. For anything that does not match a specified option, the `none`-option is taken.
1136+
<1> Use the person's name to select which property or operation to return.
1137+
<2> Use the person's age value to select which traversal to apply, noting that traversers matching no age values simply
1138+
pass through.
1139+
1140+
The `choose()`-step can use predicates with options to match ranges of values or other conditions.
11211141
11221142
[gremlin-groovy,modern]
11231143
----
11241144
g.V().hasLabel('person').
1125-
choose(values('name')).
1126-
option('marko', values('age')).
1127-
option(none, values('name'))
1145+
choose(values('age')).
1146+
option(P.between(26, 30), constant('younger')).
1147+
option(P.gt(30), constant('older')).
1148+
option(Pick.none, constant('unknown')) <1>
1149+
----
1150+
1151+
<1> If the person's age is between 26 and 30, classify them as 'younger', if greater than 30, classify as 'older',
1152+
otherwise 'unknown'.
1153+
1154+
The token `T.label` can be used as shorthand for `__.label()` when selecting options based on element labels.
1155+
1156+
[gremlin-groovy,modern]
1157+
----
1158+
g.V().choose(T.label).
1159+
option('person', out('created')).
1160+
option('software', in('created')).
1161+
values('name') <1>
11281162
----
11291163
1164+
<1> For person vertices, traverse to the software they created; for software vertices, traverse to the people who
1165+
created them.
1166+
1167+
The `Pick` enum was introduced in an example earlier to handle non-matching scenarios. The following `Pick` options may
1168+
be used with `choose()`:
1169+
1170+
* `Pick.none` - Matches when no other options match
1171+
* `Pick.unproductive` - Matches when the choice in `choose()` produces no results
1172+
1173+
[gremlin-groovy,modern]
1174+
----
1175+
g.V().choose(values('age')).
1176+
option(P.between(26, 30), values('name')).
1177+
option(Pick.none, values('name')).
1178+
option(Pick.unproductive, label()) <1>
1179+
g.V().hasLabel('person').
1180+
choose(out('knows').count()).
1181+
option(0, constant('noFriends')).
1182+
option(Pick.none, constant('hasFriends')) <2>
1183+
g.V().choose(values('age')).
1184+
option(27, __.in().values('name')).
1185+
option(32, __.out().values('name')).
1186+
option(Pick.unproductive, discard()).
1187+
option(Pick.none, discard()) <3>
1188+
----
1189+
1190+
<1> For vertices with age between 26-30, return the name. For vertices with age outside that range, return the name.
1191+
For vertices without an age property, return the label.
1192+
<2> For people with no outgoing "knows" edges, return 'noFriends', otherwise return 'hasFriends'.
1193+
<3> Use `none()` step in combination with `Pick.none` and `Pick.unproductive` to filter unproductive traversals and
1194+
unmatched values.
1195+
1196+
IMPORTANT: It is important to think of `choose()` as a branching step and not a filter. The if-then semantics can
1197+
intuitively lead to thinking the latter, where no match would mean to remove the traverser from the stream. As shown in
1198+
the examples, this is not what happens.
1199+
1200+
The `choose()`-step can be used within a `map()` step to apply the branching logic to each element in a collection.
1201+
1202+
[gremlin-groovy,modern]
1203+
----
1204+
g.V().hasLabel('person').
1205+
map(choose(values('age')).
1206+
option(P.between(26, 30), values('name').fold()).
1207+
option(Pick.none, values('name').fold())) <1>
1208+
----
1209+
1210+
<1> For each person, create a list containing their name, using the same traversal regardless of age.
1211+
11301212
*Additional References*
11311213
11321214
link:++https://tinkerpop.apache.org/javadocs/x.y.z/core/org/apache/tinkerpop/gremlin/process/traversal/dsl/graph/GraphTraversal.html#choose(java.util.function.Function)++[`choose(Function)`],

0 commit comments

Comments
 (0)