You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tinkerpop.apache.org by be...@apache.org on 2023/02/11 01:14:49 UTC

[tinkerpop] 01/01: Added Proposal 3 for removing the need for closures in Gremlin

This is an automated email from the ASF dual-hosted git repository.

bechbd pushed a commit to branch proposal_3
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git

commit 7bd0df22e3774c1a8a1bddbb035893176450eb78
Author: Dave Bechberger <da...@bechberger.com>
AuthorDate: Sat Feb 11 01:14:21 2023 +0000

    Added Proposal 3 for removing the need for closures in Gremlin
---
 docs/src/dev/future/index.asciidoc                 |    1 +
 .../dev/future/proposal-3-remove-closures.asciidoc | 1480 ++++++++++++++++++++
 2 files changed, 1481 insertions(+)

diff --git a/docs/src/dev/future/index.asciidoc b/docs/src/dev/future/index.asciidoc
index a89f9bbaaf..f849877b4a 100644
--- a/docs/src/dev/future/index.asciidoc
+++ b/docs/src/dev/future/index.asciidoc
@@ -97,6 +97,7 @@ story.
 |Proposal |Description |Targets |Resolved
 |link:https://github.com/apache/tinkerpop/blob/master/docs/src/dev/future/proposal-equality-1.asciidoc[Proposal 1] |Equality, Equivalence, Comparability and Orderability Semantics - Documents existing Gremlin semantics along with clarifications for ambiguous behaviors and recommendations for consistency. |3.6.0 |N
 |link:https://github.com/apache/tinkerpop/blob/master/docs/src/dev/future/proposal-arrow-flight-2[Proposal 2] |Gremlin Arrow Flight. |4.0.0 |N
+|link:https://github.com/apache/tinkerpop/blob/master/docs/src/dev/future/proposal-3-remove-closures[Proposal 3] |Removing the Need for Closures/Lambda in Gremlin |3.7.0 |N
 |=========================================================
 
 = Appendix
diff --git a/docs/src/dev/future/proposal-3-remove-closures.asciidoc b/docs/src/dev/future/proposal-3-remove-closures.asciidoc
new file mode 100644
index 0000000000..91214cac73
--- /dev/null
+++ b/docs/src/dev/future/proposal-3-remove-closures.asciidoc
@@ -0,0 +1,1480 @@
+////
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+////
+== Proposal 3 - Removing the Need for Closures/Lambda in Gremlin
+
+=== Status
+
+In Discussion
+
+*Action Items*
+
+* Look at defining the semanctics around type exception
+* Look at semantics around support for Unicode (talk to Greg)
+
+=== Motivation
+
+There are a number of useful operations that Gremlin users often wish to
+perform that are not provided today in the form of traversal steps or
+predicates (P/TextP). For historical reasons these functions were
+omitted and users were able to accomplish these tasks by specifying
+anonymous code blocks or “closures” to perform these tasks. For example,
+below is an example of how you can achieve a case-insensitive search for
+any cities that contain “Miami”.
+
+....
+g.V().hasLabel('city').
+    has('name',filter{it.get().toLowerCase().contains('Miami'.toLowerCase())})
+....
+
+While this is just one example of how closures are used, they are a
+powerful fallback mechanism in Gremlin to handle use cases where there
+is no functionality within the Gremlin language to meet the
+requirements. However, for a variety of reasons such as security and
+performance, many/most remote providers of TinkerPop do not allow users
+to execute closures as part of a query. This leaves users with a
+problem, as the mechanism provided to solve these sorts of use cases is
+not allowed. Examples of some commonly requested functionality that
+cannot be accomplished without the use of closures would be:
+
+[cols=",,",options="header",]
+|===
+|String Functions |List Functions |Date Functions
+|asString |reverse |dateAdd
+|concat |remove |dateDiff
+|length |indexOf |asDate
+|split |product |
+|substring |all |
+|rTrim |any |
+|lTrim |none |
+|trim |concat |
+|replace |length |
+|reverse |intersect |
+|toUpper |difference |
+|toLower |union |
+|===
+
+=== Considerations
+
+* Adding full support for traversals as parameters to predicates would
+simplify the syntax of these examples. However, this is a known issue
+with Gremlin and none of the proposed options below are blocked by it,
+nor do they exacerbated this issue any further. As such, the
+ramifications of that change are not covered by this proposal.
+
+=== Proposed Options and Recommendation
+
+==== Option 1 (Recommended)
+
+Create a new Gremlin step for each of the desired functions a user is
+looking to perform. Each step encapsulates a set of functionality that
+customers are looking to achieve. While certain steps may be reused
+across input types (e.g. `reverse()` for both list and string inputs)
+the behaviors of each step is well-defined for a given input type.
+
+*Example*: Find me all `city` nodes with a `name` starting with `miami`,
+ignoring case?
+`g.V().hasLabel('city').where(values('name').toLower(), eq('miami'))`
+
+===== Pros:
+
+* Most similar to current Gremlin patterns for adding steps
+* Feel the most like Gremlin when writing the
+
+===== Cons:
+
+* Would result in a potentially large number of steps being added to the
+language, hindering discoverability
+* Adds complexity to creating and maintaining all the current GLVs due
+to the number of new steps.
+
+==== Option 2
+
+Create a single Gremlin step that take the operation as a parameter and
+uses that parameter to mutate the behavior to achieve the desired
+functionality.
+
+*Example*: : Find me all `city` nodes with a `name` starting with
+`miami`, ignoring case?
+`g.V().hasLabel('city').where(F.apply(Func.toLower, values('name')), eq('miami'))`
+
+===== Pros:
+
+* Single step simplifies adding new operations across GLVs
+
+===== Cons:
+
+* Introduces a novel concept, a function, to the Gremlin language
+* No set signature for `F.apply` as it will differ per operation
+
+==== Option 3
+
+Create a new type of predicate in Gremlin that specifies the operation
+and returns the correct output. This would be a new paradigm within
+Gremlin, as it extends predicates to return non-boolean results.
+
+*Example*:: Find me all `city` nodes with a `name` starting with
+`miami`, ignoring case?
+`g.V().hasLabel('city').where(SP.toLower(values('name')).is('miami'))`
+
+===== Pros:
+
+* Single predicate simplifies adding new operations as they are now a
+token and not a new step that needs to be propagated across all the GLVs
+
+===== Cons:
+
+* New paradigm in Gremlin that further blurs the line between predicates
+and steps
+* Signature differs for each predicate operation
+* Signatures of common steps needs to change to include options like
+`where(Predicate)`
+
+=== Proposed Syntax
+
+<<string-function-syntax>>
+
+<<list-function-syntax>>
+
+<<date-function-syntax>>
+
+=== Examples
+
+==== String Examples
+
+===== String Example 1 (SE1)
+
+I want to find the offices, by name, where the name does not have a "-"
+as the third character of the string
+(https://stackoverflow.com/questions/56115935/gremlin-is-there-a-way-to-find-the-character-based-on-the-index-of-a-string[here])
+
+`g.V().hasLabel('office').where(__.values('name').substr(2, 1)).is(neq('-'))) `
+
+===== String Example 2 (SE2)
+
+I would like to trim out the "Mbit/s" from the string
+(https://stackoverflow.com/questions/45365726/im-unable-to-substring-values-that-i-get-by-running-a-gremlin-query-ive-been[here])
+
+`g.V('Service').has('serviceId','ETHA12819844').out('AssociatedToService').`
+`value("bandwidth").replace("Mbit/s", "")`
+
+===== String Example 3 (SE3)
+
+I am trying to add a new vertex which should be labeled like an existing
+vertex but with some prefix attached
+(https://stackoverflow.com/questions/61106927/concatenate-gremlin-graphtraversal-result-with-string[here])
+
+....
+`g.V(3).as('a').addV(constant("").concat("prefix_", select('a').label())`
+....
+
+===== String Example 4 (SE4)
+
+Find all products that start with the same case-insensitive prefix. +
+e.g. Given the following products:
+
+[cols=",",options="header",]
+|===
+|id |product_name
+|1 |PROD-123
+|2 |PROD-234
+|3 |TEST-1234
+|4 |GAMMA-1234
+|5 |PR-123
+|===
+
+We should return:
+
+[cols=",",options="header",]
+|===
+|id |product_name
+|1 |PROD-123
+|2 |PROD-234
+|===
+
+....
+g.V().hasLabel('Product').has('product_name').as('product1').
+  V().hasLabel('Product').has('product_name'`).`
+  where(__.is(select('product1').toLower())`.values('product_name').substring(0, 5)).
+  select('product1')
+....
+
+===== String Example 5 (SE5)
+
+Perform case-insensitive search
+
+....
+g.V().hasLabel('Product').where(values('product_name').toLower(), eq('foo'))
+....
+
+===== String Example 6 (SE6)
+
+Applying functions to returning values, in this case return the `age`
+and a lower cased version of `name`
+
+`g.V().hasLabel('person').valueMap('age', 'name').by().by(toLower())`
+
+===== String Example 7 (SE7)
+
+Concatenating values on the return, in this case return a concatenated
+name
+
+`g.V().hasLabel('person').project('age', 'name').` `by('age').`
+`by(values('first_name').concat(" ").concat(values('last_name'))`
+
+==== List Examples
+
+===== List Example 1 (LE1)
+
+Given a list of people, return the list of `age`s if everyone’s `age` >
+18
+
+`g.V().hasLabel('person').values('age').fold().where(all(gt(18)))`
+
+===== List Example 2 (LE2)
+
+Given a set of vertices, return the list of vertices if anyone’s `age` >
+18
+
+`g.V([1,2,3,4]).fold().where(any(values('age').is(gt(18))))`
+
+===== List Example 3 (LE3)
+
+Given a list, find the index of the first occurrence of `Dave`
+
+`g.V().hasLabel('person').fold().indexOf(has('name', 'Dave'))` `==> 12`
+
+`g.inject(['Dave', 'Kelvin', 'Stephen']).indexOf(constant('Dave'))`
+`==> 0`
+
+===== List Example 4 (LE4)
+
+Given a list of people, remove any person with a name of `Dave`
+
+`g.V().hasLabel('person').fold().remove(has('name', 'Dave'))`
+`==> [‘Kelvin’, ‘Stephen’]`
+
+`g.inject(['Dave', 'Kelvin', 'Stephen']).remove(constant('Dave'))`
+`==> [‘Kelvin’, ‘Stephen’]`
+
+`g.inject(['Dave', 'Kelvin', 'Stephen']).remove(constant(['Dave', 'Stephen'))`
+`==> ['Kelvin']`
+
+==== Date Examples
+
+===== Date Example 1 (DE1)
+
+Given a transaction, find me all other transactions within 7 days prior
+
+`g.V('transaction1').values('date').dateAdd(DT.Days, -7).as('purchase_date').V().hasLabel('transaction').where(gt('purchase_date')).by('date').by()`
+
+===== Date Example 2 (DE2)
+
+Given two transactions, find me the difference in the dates
+
+`g.V('transaction1').values('date').dateDiff(DT.Days, V('transaction2').values('date').asDate())`
+
+===== Date Example 3 (DE3)
+
+Given a static value, return me the value as a date
+
+`g.inject('1900-01-01').asDate()`
+
+===== Date Example 4 (DE4)
+
+Find the difference between a transaction and the first of the year
+
+`g.V('transaction1').values('date').dateDiff(DT.Days, inject(datetime('2023-01-01'))`
+
+
+
+== String Manipulation functions in TinkerPop [[string-function-syntax]]
+
+One of the common gaps that user's find when using Gremlin is that there
+is a lack of string manipulation capabilities within the language
+itself. This requires that users use closures to handle many common
+string manipulation options that users want to do on data in the graph.
+This is a problem for many users as many of the providers prevent the
+use of arbitrary closures due to the security risks so for these users
+there is no way to manipulate strings directly.
+
+=== Proposal
+
+The proposal here is to add a set of steps to handle common string
+manipulation requests from users, the details for each are discussed
+below:
+
+* https://gist.github.com/bechbd/6de5d3d81fdb765166e435d961b0ccef#asstring[asString()]
+* https://gist.github.com/bechbd/6de5d3d81fdb765166e435d961b0ccef#concat[concat()]
+* https://gist.github.com/bechbd/6de5d3d81fdb765166e435d961b0ccef#length[length()]
+* https://gist.github.com/bechbd/6de5d3d81fdb765166e435d961b0ccef#split[split()]
+* https://gist.github.com/bechbd/6de5d3d81fdb765166e435d961b0ccef#substring[substring()]
+* https://gist.github.com/bechbd/6de5d3d81fdb765166e435d961b0ccef#rtrim[rTrim()]
+* https://gist.github.com/bechbd/6de5d3d81fdb765166e435d961b0ccef#ltrim[lTrim()]
+* https://gist.github.com/bechbd/6de5d3d81fdb765166e435d961b0ccef#trim[trim()]
+* https://gist.github.com/bechbd/6de5d3d81fdb765166e435d961b0ccef#replace[replace()]
+* https://gist.github.com/bechbd/6de5d3d81fdb765166e435d961b0ccef#reverse[reverse()]
+* https://gist.github.com/bechbd/6de5d3d81fdb765166e435d961b0ccef#toupper[toUpper()]
+* https://gist.github.com/bechbd/6de5d3d81fdb765166e435d961b0ccef#tolower[toLower()]
+
+=== Gremlin Language Variant Function Names
+
+[cols=",,,,,",options="header",]
+|===
+|Groovy |Java |Python |JavaScript |.NET |Go
+|asString() |asString() |asString() |asString() |AsString() |AsString()
+
+|concat() |concat() |concat() |concat() |Concat() |Concat()
+
+|length() |length() |length() |length() |Length() |Length()
+
+|split() |split() |split() |split() |Split() |Split()
+
+|substring() |substring() |substring() |substring() |Substring()
+|Substring()
+
+|rTrim() |rTrim() |rTrim() |rTrim() |RTrim() |RTrim()
+
+|lTrim() |lTrim() |lTrim() |lTrim() |LTrim() |LTrim()
+
+|trim() |trim() |trim() |trim() |Trim() |Trim()
+
+|replace() |replace() |replace() |replace() |Replace() |Replace()
+
+|reverse() |reverse() |reverse() |reverse() |Reverse() |Reverse()
+
+|toUpper() |toUpper() |toUpper() |toUpper() |ToUpper() |ToUpper()
+
+|toLower() |toLower() |toLower() |toLower() |ToLower() |ToLower()
+|===
+
+'''''
+
+== Function Definitions
+
+=== `asString()`
+
+Returns the value of the incoming traverser as a string
+
+==== Signature(s)
+
+`asString()`
+
+`asString(Scope)`
+
+==== Parameters
+
+* Scope - Scope Enum
+
+==== Allowed incoming traverser types
+
+Any data type allowed by TinkerPop
+
+==== Expected Output
+
+A String value representing the string value of the traverser being
+passed in as shown below:
+
+[cols=",,",options="header",]
+|===
+|Incoming Datatype |Example Query |Example Output
+|Integer |`g.inject(29).asString()` |29
+
+|Float |`g.inject(29.0).asString()` |29.0
+
+|String |`g.inject('foo').asString()` |foo
+
+|UUID |`g.inject(UUID.randomUUID()).asString()`
+|47557eed-04e7-4aa4-89eb-9689d26fe94a
+
+|Map
+|`g.inject([["id": 1], ["id": 2, "something":"anything"]]).asString()`
+|[[id:1], [id:2, something:anything]]
+
+|Date |`g.inject(datetime()).asString()` |Sun Nov 04 00:00:00 UTC 2018
+
+|List |`g.inject([1,2,3]).asString()` |[1, 2, 3]
+
+|List (Local Scope) |`g.inject([1,2,3]).asString(local)` |["1", "2",
+"3"]
+
+|Vertex |`g.V(1).asString()` |v[1]
+
+|Edge |`g.E(7).asString()` |e[7][1-knows->2]
+
+|Property |`g.V(1).properties('age').asString()` |vp[age->29]
+
+|null |`g.V().group().by('foo').select(keys).asString()` |null
+|===
+
+'''''
+
+=== `concat()`
+
+Concatenates one or more strings together
+
+==== Signature(s)
+
+`concat(String...)`
+
+`concat(Traversal)`
+
+`concat(Scope, String...)`
+
+`concat(Scope, Traversal)`
+
+==== Parameters
+
+* String... - One or more String values to concatenate to the input
+string
+* Traversal - A traversal value to concatenate
+* Scope - Scope Enum
+
+==== Allowed incoming traverser types
+
+String data types or array, if local scope is used. If a non-string
+traverser, or the list containing non-string values, is passed in then
+an `IllegalArgumentException` will be thrown
+
+==== Expected Output
+
+A String value representing the concatenation of all the
+
+....
+g.inject('this').concat('is', 'a', 'test')
+==>thisisatest
+g.V(1).values('first_name').concat(' ').concat(V(1).values('last_name')
+==>John Doe
+g.inject(['this', 'is', 'a', 'test']).concat(local)
+==>thisisatest
+g.inject(['John', ' ']).concat(local, V(1).values('last_name')
+==>John Doe
+....
+
+*Note* `concat()` may also be extended to handle concatenating list
+values together but that is out of scope for this change.
+
+'''''
+
+=== `length()`
+
+Returns the length of the input string
+
+==== Signature(s)
+
+`length()`
+
+`length(Scope)`
+
+==== Parameters
+
+* Scope - Scope Enum
+
+==== Allowed incoming traverser types
+
+String data types or array, if local scope is used. If a non-string
+traverser, or the list containing non-string values, is passed in then
+an `IllegalArgumentException` will be thrown
+
+==== Expected Output
+
+A Long value representing the number of items in an array or the number
+of characters in a string
+
+....
+g.inject('this').length()
+==>4
+g.inject('this').length(local)
+==>4
+....
+
+*Note*:While this is similar to `count(local)` they are not the same.
+`count(local)` treats the input by calculating the count of the items
+stored within the traversal. `length()` treats the input as an array and
+provides the length of that array.
+
+[cols=",,,",options="header",]
+|===
+|Input Datatype |Example traversal |count(local) |length()
+|Integer |`g.inject(29)` |1 |IllegalArgumentException
+
+|Float |`g.inject(29.0)` |1 |IllegalArgumentException
+
+|String |`g.inject('foo')` |1 |3
+
+|UUID |`g.inject(UUID.randomUUID())` |1 |IllegalArgumentException
+
+|Map |`g.inject(["id": 2, "something":"anything"]])` |1
+|IllegalArgumentException
+
+|Date |`g.inject(datetime())` |1 |IllegalArgumentException
+
+|List |`g.inject([1,2,3])` |3 |3
+
+|Vertex |`g.V(1)` |1 |IllegalArgumentException
+
+|Edge |`g.E(7)` |1 |IllegalArgumentException
+
+|Property |`g.V(1).properties('age')` |1 |IllegalArgumentException
+
+|null |`g.V().group().by('foo').select(keys)` |0
+|IllegalArgumentException
+|===
+
+'''''
+
+=== `split()`
+
+Returns a list of strings created by splitting the input string around
+the matches of the given delimiter.
+
+==== Signature(s)
+
+`split(String)`
+
+`split(Scope, String)`
+
+==== Parameters
+
+* String - The delimiter character(s) to split the input string* *
+
+==== Allowed inputs
+
+String data types or array, if local scope is used. If a non-string
+traverser, or the list containing non-string values, is passed in then
+an `IllegalArgumentException` will be thrown
+
+==== Expected Output
+
+An array of strings split around the delimiter character(s)
+
+....
+g.inject('this').split('h')
+==>[t, is]
+g.inject('one,two').split(',')
+==>[one, two]
+g.inject('axxb').split('x')
+==>[a, b]
+g.inject('axybxc').split('xy')
+==>[a, bxc]
+g.inject(['this', 'that']).split('h')
+==>[[t, is], [t, at]]
+....
+
+'''''
+
+=== `substring()`
+
+returns a substring of the original string with the length specified,
+uses a 0-based start
+
+==== Signature(s)
+
+`substring(Long, Long)`
+
+`substring(Long)`
+
+`substring(Scope, Long, Long)`
+
+`substring(Scope, Long)`
+
+==== Parameters
+
+* Long - The start index, 0 based. If the value is negative then the
+start location will be the end of the string and it will go the
+specified number of characters from the end of the string.
+* Long - The number of characters to return. Optional - if not provided
+then all remaining characters will be returned
+* Scope - Scope Enum
+
+==== Allowed incoming traverser types
+
+String data types or array, if local scope is used. If a non-string
+traverser, or the list containing non-string values, is passed in then
+an `IllegalArgumentException` will be thrown
+
+==== Expected Output
+
+A String value containing the number of characters specified beginning
+at the start location. If the start location plus the length specified
+is greater than or equal to the input length, the result will contain
+the entire string.
+
+....
+g.inject('this').substring(0, 1)
+==>t
+g.inject('this').substring(2)
+==>is
+g.inject('this').substring(2, 5)
+==>is
+g.inject('this').substring(-1)
+==>s
+g.inject(['this', 'is', 'a', 'test']).substring(local, 2)
+==>[is, '' ,'' , 'st']
+....
+
+'''''
+
+=== `rTrim()`
+
+Returns a string with trailing whitespace removed
+
+*Note*: Whitespace characters are defined as space/tab/line feed/line
+tabulation/form feed/carriage return.
+
+==== Signature(s)
+
+`rTrim()`
+
+`rTrim(Scope)`
+
+==== Parameters
+
+* Scope - Scope Enum
+
+==== Allowed incoming traverser types
+
+String data types or array, if local scope is used. If a non-string
+traverser, or the list containing non-string values, is passed in then
+an `IllegalArgumentException` will be thrown
+
+==== Expected Output
+
+A string value with trailing whitespace removed
+
+....
+g.inject('this ').rTrim()
+==>this
+g.inject(['this ', 'that ']).rTrim(local)
+==>[this, that]
+....
+
+'''''
+
+=== `lTrim()`
+
+Returns a string with leading whitespace removed
+
+*Note*: Whitespace characters are defined as space/tab/line feed/line
+tabulation/form feed/carriage return.
+
+==== Signature(s)
+
+`lTrim()`
+
+`lTrim(Scope)`
+
+==== Parameters
+
+* Scope - Scope Enum
+
+==== Allowed incoming traverser types
+
+String data types or array, if local scope is used. If a non-string
+traverser, or the list containing non-string values, is passed in then
+an `IllegalArgumentException` will be thrown
+
+==== Expected Output
+
+A string value with leading whitespace removed
+
+....
+g.inject(' this').lTrim()
+==>this
+g.inject([' this', ' that']).lTrim(local)
+==>[this, that]
+....
+
+'''''
+
+=== `trim()`
+
+Returns a string with leading and trailing whitespace removed
+
+*Note*: Whitespace characters are defined as space/tab/line feed/line
+tabulation/form feed/carriage return.
+
+==== Signature(s)
+
+`trim()`
+
+`trim(Scope)`
+
+==== Parameters
+
+* Scope - Scope Enum
+
+==== Allowed incoming traverser types
+
+String data types or array, if local scope is used. If a non-string
+traverser, or the list containing non-string values, is passed in then
+an `IllegalArgumentException` will be thrown
+
+==== Expected Output
+
+A string value with leading and trailing whitespace removed
+
+....
+g.inject(' this ').trim()
+==>this
+g.inject([' this ', ' that ']).trim()
+==>[this, that]
+....
+
+'''''
+
+=== `replace()`
+
+Returns a string with the specified characters in the original string
+replaced with the new characters
+
+==== Signature(s)
+
+`replace(String, String)`
+
+`replace(Scope, String, String)`
+
+==== Parameters
+
+* String - The character(s) to be replaced
+* String - The character(s) to replace with
+* Scope - Scope Enum
+
+==== Allowed incoming traverser types
+
+String data types or array, if local scope is used. If a non-string
+traverser, or the list containing non-string values, is passed in then
+an `IllegalArgumentException` will be thrown
+
+==== Expected Output
+
+A string
+
+....
+g.inject('this').replace('t', 'x)
+==>xhis
+g.inject('this').replace('x', 't')
+==>this
+g.inject('this').replace('is', 'was')
+==>thwas
+g.inject(['this', 'that']).replace('th', 'was')
+==>[wasis, wasat]
+....
+
+'''''
+
+=== `reverse()`
+
+Reverses the current string
+
+==== Signature(s)
+
+`reverse()`
+
+`reverse(Scope)`
+
+==== Parameters
+
+* Scope - Scope Enum
+
+==== Allowed incoming traverser types
+
+String data types or array, if local scope is used. If a non-string
+traverser, or the list containing non-string values, is passed in then
+an `IllegalArgumentException` will be thrown
+
+==== Expected Output
+
+A String value representing the reversed version of the incoming string
+
+....
+g.inject('this').reverse()
+==>siht
+g.inject(['this', 'that']).reverse(local)
+==>[siht, taht]
+....
+
+*Note* `reverse()` may also be extended to handle concatenating list
+values together but that is out of scope for this change.
+
+'''''
+
+=== `toUpper()`
+
+Returns an upper case string representation.
+
+*Note*: All case conversions will be done via the mappings specified for
+Unicode (https://www.unicode.org/reports/tr44/#Casemapping[found here])
+
+==== Signature(s)
+
+`toUpper()`
+
+`toUpper(Scope)`
+
+==== Parameters
+
+* Scope - Scope Enum
+
+==== Allowed incoming traverser types
+
+String data types or array, if local scope is used. If a non-string
+traverser, or the list containing non-string values, is passed in then
+an `IllegalArgumentException` will be thrown
+
+==== Expected Output
+
+A string
+
+....
+g.inject('this').toUpper()
+==>THIS
+g.inject(['this', 'that']).toUpper()
+==>[THIS, THAT]
+....
+
+'''''
+
+=== `toLower()`
+
+Returns an lower case string representation
+
+*Note*: All case conversions will be done via the mappings specified for
+Unicode (https://www.unicode.org/reports/tr44/#Casemapping[found here])
+
+==== Signature(s)
+
+`toLower()`
+
+`toLower(Scope)`
+
+==== Parameters
+
+* Scope - Scope Enum
+
+==== Allowed incoming traverser types
+
+String data types or array, if local scope is used. If a non-string
+traverser, or the list containing non-string values, is passed in then
+an `IllegalArgumentException` will be thrown
+
+==== Expected Output
+
+A string
+
+....
+g.inject('THIS').toLower()
+==>this
+g.inject(['THIS', 'THAT']).toLower()
+==>[this, that]
+....
+
+
+== List Manipulation functions in TinkerPop [[list-function-syntax]]
+
+One of the common gaps that user's find when using Gremlin is that there
+is a lack of list manipulation capabilities within the language itself.
+This requires that users use closures to handle many common manipulation
+options that users want to do on data in the graph. This is a problem
+for many users as many of the providers prevent the use of arbitrary
+closures due to the security risks so for these users there is no way to
+manipulate strings directly.
+
+=== Proposal
+
+The proposal here is to add a set of steps to handle common list
+manipulation requests from users, the details for each are discussed
+below:
+
+* https://gist.github.com/bechbd/6de5d3d81fdb765166e435d961b0ccef#length[length()]
+* https://gist.github.com/bechbd/6de5d3d81fdb765166e435d961b0ccef#reverse[reverse()]
+* https://gist.github.com/bechbd/6de5d3d81fdb765166e435d961b0ccef#remove[remove()]
+* https://gist.github.com/bechbd/6de5d3d81fdb765166e435d961b0ccef#indexOf[indexOf()]
+* https://gist.github.com/bechbd/6de5d3d81fdb765166e435d961b0ccef#product[product()]
+* https://gist.github.com/bechbd/6de5d3d81fdb765166e435d961b0ccef#all[all()]
+* https://gist.github.com/bechbd/6de5d3d81fdb765166e435d961b0ccef#any[any()]
+* https://gist.github.com/bechbd/6de5d3d81fdb765166e435d961b0ccef#none[none()]
+* https://gist.github.com/bechbd/6de5d3d81fdb765166e435d961b0ccef#concat[concat()]
+* https://gist.github.com/bechbd/6de5d3d81fdb765166e435d961b0ccef#intersect[intersect()]
+* https://gist.github.com/bechbd/6de5d3d81fdb765166e435d961b0ccef#union[union()]
+* https://gist.github.com/bechbd/6de5d3d81fdb765166e435d961b0ccef#difference[difference()]
+
+=== Gremlin Language Variant Function Names
+
+[cols=",,,,,",options="header",]
+|===
+|Groovy |Java |Python |JavaScript |.NET |Go
+|length() |length() |length() |length() |Length() |Length()
+
+|reverse() |reverse() |reverse() |reverse() |Reverse() |Reverse()
+
+|remove() |remove() |remove() |remove() |Remove() |Remove()
+
+|indexOf() |indexOf() |indexOf() |indexOf() |IndexOf() |IndexOf()
+
+|product() |product() |product() |product() |Product() |Product()
+
+|all() |all() |all() |all() |All() |All()
+
+|any() |any() |any() |any() |Any() |Any()
+
+|none() |none() |none() |none() |None() |None()
+
+|concat() |concat() |concat() |concat() |Concat() |Concat()
+
+|intersect() |intersect() |intersect() |intersect() |Intersect()
+|Intersect()
+
+|union() |union() |union() |union() |Union() |Union()
+
+|difference() |difference() |difference() |difference() |Difference()
+|Difference()
+|===
+
+'''''
+
+== Function Definitions
+
+=== `length()`
+
+Returns the length of a list in the incoming traverser
+
+==== Signature(s)
+
+`length()`
+
+==== Parameters
+
+None
+
+==== Allowed incoming traverser types
+
+Array data types. If non-array data types are passed in then an
+`IllegalArgumentException` will be thrown
+
+==== Expected Output
+
+A Long value representing the number of items in an array or the number
+of characters in a string
+
+....
+g.inject([1, 2]).length()
+==>2
+....
+
+=== `reverse()`
+
+Returns the value of the incoming list in reverse order
+
+==== Signature(s)
+
+`reverse()`
+
+==== Parameters
+
+None
+
+==== Allowed incoming traverser types
+
+Array data types. If non-array data types are passed in then an
+`IllegalArgumentException` will be thrown
+
+==== Expected Output
+
+An array in reverse order.
+
+....
+g.inject([1,2]).reverse()
+==>[2, 1]
+....
+
+=== `remove()`
+
+Removes the first element from the incoming list where the value equals
+the specified value
+
+==== Signature(s)
+
+`remove(value)`
+
+`remove(Traversal)`
+
+==== Parameters
+
+* value - The value to remove
+
+==== Allowed incoming traverser types
+
+Array data types. If non-array data types are passed in then an
+`IllegalArgumentException` will be thrown
+
+==== Expected Output
+
+An array value representing the new list
+
+....
+g.inject([1,2]).remove(1)
+==>[2]
+....
+
+=== `indexOf()`
+
+Returns the first occurrence of the `value` in the incoming array
+
+==== Signature(s)
+
+`indexOf(value)`
+
+`indexOf(Traversal)`
+
+==== Parameters
+
+* value - The value to locate
+
+==== Allowed incoming traverser types
+
+Array data types. If non-array data types are passed in then an
+`IllegalArgumentException` will be thrown
+
+==== Expected Output
+
+A long representing the index of the first occurrence of the value
+(zero-based). If the values does not exist then `null` is returned
+
+....
+g.inject([1,2]).indexOf(1)
+==>0
+....
+
+=== `product()`
+
+Returns the cartesian product of two lists
+
+==== Signature(s)
+
+`product(value)`
+
+`product(Traversal)`
+
+==== Parameters
+
+* value - An array
+
+==== Allowed incoming traverser types
+
+Array data types. If non-array data types are passed in then an
+`IllegalArgumentException` will be thrown
+
+==== Expected Output
+
+A set of values where each value contains the cartesian product of two
+lists
+
+....
+g.inject([1,2]).product([3,4])
+==>[[1,3], [1,4], [2,3], [2,4]]
+....
+
+=== `any()`
+
+Returns true if any items in the array `value` exist in the input
+
+==== Signature(s)
+
+`any(value)`
+
+`any(Traversal)`
+
+==== Parameters
+
+* value - An array of the items to check in the incoming list
+
+==== Allowed incoming traverser types
+
+Array data types. If non-array data types are passed in then an
+`IllegalArgumentException` will be thrown
+
+==== Expected Output
+
+True if any values from one list are in the other, False otherwise
+
+....
+g.inject([1,2]).any([1])
+==>true
+g.inject([1,2]).any([3])
+==>false
+....
+
+=== `all()`
+
+Returns true if all items in the array `value` exist in the input
+
+==== Signature(s)
+
+`all(value)`
+
+`all(Traversal)`
+
+==== Parameters
+
+* value - An array of the items to check in the incoming list
+
+==== Allowed incoming traverser types
+
+Array data types. If non-array data types are passed in then an
+`IllegalArgumentException` will be thrown
+
+==== Expected Output
+
+True if all values from one list are in the other, False otherwise
+
+....
+g.inject([1,2]).all([1])
+==>true
+g.inject([1,2]).all([1, 3])
+==>false
+g.inject([1,2]).all([3])
+==>false
+....
+
+=== `none()`
+
+Returns true if all items in the array `value` exist in the input
+
+==== Signature(s)
+
+`none(value)`
+
+`none(Traversal)`
+
+==== Parameters
+
+* value - An array of the items to check in the incoming list
+
+==== Allowed incoming traverser types
+
+Array data types. If non-array data types are passed in then an
+`IllegalArgumentException` will be thrown
+
+==== Expected Output
+
+True if no values from one list are in the other, False otherwise
+
+....
+g.inject([1,2]).none([1])
+==>false
+g.inject([1,2]).none([1, 3])
+==>false
+g.inject([1,2]).none([3])
+==>true
+....
+
+=== `concat()`
+
+Returns the concatenation of the incoming array and the traversal or
+array value passed as a parameter. This will return all values,
+including duplicates.
+
+==== Signature(s)
+
+`concat(value)`
+
+`concat(Traversal)`
+
+==== Parameters
+
+* value - An array of the items to check in the incoming list
+
+==== Allowed incoming traverser types
+
+Array data types. If non-array data types are passed in then an
+`IllegalArgumentException` will be thrown
+
+==== Expected Output
+
+An array containing the values of the concatenation of the two lists
+
+....
+g.inject([1,2]).concat([3])
+==>[1, 2, 3]
+g.inject([1,2]).concat([1, 4])
+==>[1, 2, 1, 4]
+g.V().has('age', 29).values('age').dedup().fold().concat(V().has('age', 30).values('age').dedup().fold())
+==>[29, 30]
+....
+
+=== `union()`
+
+Returns the union of the incoming array and the traversal or array value
+passed as a parameter. This will return an array of unique values
+
+==== Signature(s)
+
+`union(value)`
+
+`union(Traversal)`
+
+==== Parameters
+
+* value - An array of the items to check in the incoming list
+
+==== Allowed incoming traverser types
+
+Array data types. If non-array data types are passed in then an
+`IllegalArgumentException` will be thrown
+
+==== Expected Output
+
+An array containing the unique values of the union of the two lists
+
+....
+g.inject([1,2]).union([1])
+==>[1, 2]
+g.inject([1,2]).union([1, 4])
+==>[1, 2, 4]
+g.V().has('age', 29).values('age').dedup().fold().union(V().has('age', 30).values('age').dedup().fold())
+==>[29, 30]
+....
+
+=== `intersect()`
+
+Returns the intersection of the incoming array and the traversal or
+array value passed as a parameter. This will return an array of unique
+values
+
+==== Signature(s)
+
+`intersect(value)`
+
+`intersect(Traversal)`
+
+==== Parameters
+
+* value - An array of the items to check in the incoming list
+
+==== Allowed incoming traverser types
+
+Array data types. If non-array data types are passed in then an
+`IllegalArgumentException` will be thrown
+
+==== Expected Output
+
+An array containing the unique values of the intersection of the two
+lists
+
+....
+g.inject([1,2]).intersect([1])
+==>[1]
+g.inject([1,2]).intersect([1, 2, 3])
+==>[1, 2]
+g.V().has('age', 29).values('age').dedup().fold().intersect(V().has('age', 30).values('age').dedup().fold())
+==>[]
+....
+
+=== `difference()`
+
+Returns the difference of the incoming array and the traversal or array
+value passed as a parameter. This will return an array of unique values
+
+==== Signature(s)
+
+`difference(value)`
+
+`difference(Traversal)`
+
+==== Parameters
+
+* value - An array of the items to check in the incoming list
+
+==== Allowed incoming traverser types
+
+Array data types. If non-array data types are passed in then an
+`IllegalArgumentException` will be thrown
+
+==== Expected Output
+
+An array containing the different values of the intersection of the two
+lists
+
+....
+g.inject([1,2]).difference([1])
+==>[2]
+g.inject([1,2]).difference([1, 2, 3])
+==>[3]
+g.V().has('age', 29).values('age').dedup().fold().difference(V().has('age', 30).values('age').dedup().fold())
+==>[29, 30]
+....
+
+
+== Date Manipulation functions in TinkerPop [[date-function-syntax]]
+
+One of the common gaps that user's find when using Gremlin is that there
+is a lack of date manipulation capabilities within the language itself.
+This requires that users use closures to handle many common manipulation
+options that users want to do on data in the graph. This is a problem
+for many users as many of the providers prevent the use of arbitrary
+closures due to the security risks so for these users there is no way to
+manipulate strings directly.
+
+=== Proposal
+
+The proposal here is to add a set of steps to handle common date
+manipulation requests from users, the details for each are discussed
+below:
+
+* https://gist.github.com/bechbd/6de5d3d81fdb765166e435d961b0ccef#asDate[asDate()]
+* https://gist.github.com/bechbd/6de5d3d81fdb765166e435d961b0ccef#dateAdd[dateAdd()]
+* https://gist.github.com/bechbd/6de5d3d81fdb765166e435d961b0ccef#dateDiff[dateDiff()]
+
+=== Gremlin Language Variant Function Names
+
+[cols=",,,,,",options="header",]
+|===
+|Groovy |Java |Python |JavaScript |.NET |Go
+|asDate() |asDate() |asDate() |asDate() |asDate() |asDate()
+|dateAdd() |dateAdd() |dateAdd() |dateAdd() |DateAdd() |DateAdd()
+|dateDiff() |dateDiff() |dateDiff() |dateDiff() |DateDiff() |DateDiff()
+|===
+
+== Function Definitions
+
+=== `asDate()`
+
+Returns the value of the incoming traverser as an ISO-8601 date
+
+==== Signature(s)
+
+`asDate()`
+
+`asDate(Scope)`
+
+==== Parameters
+
+* Scope - Scope Enum
+
+==== Allowed incoming traverser types
+
+Any data type that can be parsed into an ISO-8601 date. If an
+unsupported types is passed in then an `IllegalArgumentException` will
+be thrown
+
+==== Expected Output
+
+A Date value representing the ISO-8601 value of the traverser being
+passed in as shown below:
+
+[cols=",,",options="header",]
+|===
+|Incoming Datatype |Example Query |Example Output
+|Integer |`g.inject(0).asString()` |1900-01-01T00:00:00Z
+
+|Float |`g.inject(29.0).asString()` |1900-01-01T00:00:00Z
+
+|String |`g.inject('1/1/1900').asString()` |1900-01-01T00:00:00Z
+
+|UUID |`g.inject(UUID.randomUUID()).asString()`
+|`IllegalArgumentException`
+
+|Map
+|`g.inject([["id": 1], ["id": 2, "something":"anything"]]).asString()`
+|`IllegalArgumentException`]
+
+|Date |`g.inject(datetime()).asString()` |Sun Nov 04 00:00:00 UTC 2018
+
+|List |`g.inject([1,2,3]).asString()` |`IllegalArgumentException`
+
+|List (Local Scope) |`g.inject([1,2,3]).asString(local)`
+|[`IllegalArgumentException`
+
+|Vertex |`g.V(1).asString()` |`IllegalArgumentException`
+
+|Edge |`g.E(7).asString()` |`IllegalArgumentException`
+
+|Property |`g.V(1).properties('age').asString()`
+|`IllegalArgumentException`
+
+|null |`g.V().group().by('foo').select(keys).asString()`
+|`IllegalArgumentException`
+|===
+
+=== `dateAdd()`
+
+Returns the value with the addition of the `value` number of units as
+specified by the `DateToken`
+
+==== Signature(s)
+
+`dateAdd(DateToken, value)`
+
+`dateAdd(Scope, DateToken, value))`
+
+==== Parameters
+
+* DateToken - DateToken Enum
+* value - The number of units, specified by the Date Token, to add to
+the incoming values
+
+==== Allowed incoming traverser types
+
+Date data types. If non-array data types are passed in then an
+`IllegalArgumentException` will be thrown
+
+==== Expected Output
+
+A Date with the value added.
+
+....
+g.inject(datetime()).dateAdd(DT.days, 7)
+==> 2018-03-22
+g.inject(datetime()).dateAdd(DT.days, -7)
+==> 2018-03-8
+g.inject([datetime(), datetime()]).dateAdd(local, DT.days, 7)
+==> [2018-03-22, 2018-03-22]
+....
+
+=== `dateDiff()`
+
+Returns the difference between two dates in epoch time
+
+==== Signature(s)
+
+`dateDiff(value)`
+
+`dateDiff(Traversal)`
+
+`dateDiff(Scope, value))`
+
+==== Parameters
+
+* value - The date to find the difference from
+
+==== Allowed incoming traverser types
+
+Date data types. If non-array data types are passed in then an
+`IllegalArgumentException` will be thrown
+
+==== Expected Output
+
+The epoch time difference between the two values
+
+....
+g.inject(datetime()).dateDiff(datetime().dateAdd(DT.days, 7))
+==> 604800
+g.inject(datetime()).dateDiff(datetime().dateAdd(DT.days, 7))
+==> -604800
+g.inject([datetime(), datetime()]).dateAdd(local, DT.days, 7)
+==> [604800, 604800]
+....