Skip to content
This repository has been archived by the owner on Sep 28, 2022. It is now read-only.

Reimplementing destructuring #561

Merged
merged 18 commits into from
Oct 20, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 68 additions & 8 deletions doc/basics.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -720,18 +720,20 @@ let a, b = [1, 2]
# a = 1, b = 2
----

If there are more variables than values, an exception is raised. If there are
fewer, the remaining values are ignored. A special syntax is available to
assign the rest of the values, similar to varargs notation. For instance:
If there are more or fewer variables than values to assign, an exception is raised.
A special syntax is available to assign the rest of the values, similar to varargs notation. For instance:

[source,golo]
----
let a, b, c = [1, 2] # raises an exception
let a, b = [1, 2, 3] # a = 1, b = 2, 3 ignored
let a, b, c... = [1, 2, 3, 4, 5] # a = 1, b = 2, c = [3, 4, 5]
let a, b, c = list[1, 2] # raises an exception
let a, b = list[1, 2, 3] # raises an exception
let a, b, c... = list[1, 2, 3, 4, 5] # a = 1, b = 2, c = list[3, 4, 5]
----

Any object having a `destruct()` method returning a tuple can be used in
The reminder has the same type than the destructured object. Note that some mainly container-like object can be destructured using this syntax.
For instance, golo structures can't use the rest syntax and raise an exception.

Any object having a `__$$_destruct(number, rest, toSkip)` method returning an array can be used in
destructuring assignments. Golo specific data structures and some Java native
ones (arrays, maps, collections) can be destructured. Augmentations can be used to make an existing class
destructurable.
Expand All @@ -749,7 +751,7 @@ as well as java lists:
[source,golo]
----
let lst = list[1, 2, 3, 4, 5]
let head, tail... = lst # head = 1, tail = [2, 3, 4, 5]
let head, tail... = lst # head = 1, tail = list[2, 3, 4, 5]
----

Already defined variables can also be assigned with destructuring. For
Expand All @@ -769,6 +771,64 @@ foreach key, value in myMap: entrySet() {
}
----

It is moreover possible to ignore some of the destructured values by using the special `_` variable name.
[source,golo]
----
let a, _, _, _, b = list[1, 2, 3, 4, 5]
# a = 1 and b = 5
----

This name can be used with the remainer syntax, to avoid the creation of a new structure to hold the other values.
For instance:
[source,golo]
----
let _, a, _, b, _... = [1..500]
----

Here, `a` is `2` and `b` is `4`. No new range is created for the remaining values.

==== Destructuring method

To be destructurable, an object must have a `__$$_destruct(number, rest, skip)` method, whose parameters are:

- the number of values to be assigned to,
- whether the assignement has a rest (i.e. if remainer syntax `...` is used)
- a list describing the positions to skip (`_` special name)

and must return an array containg the values to assign (`null` if skipped).

For instance, the code:
[source,golo]
----
let a, _, b... = [8..20]
let it = list[1, 2, 3, 4, 5]: iterator()
let _, x, y, _... = it
----

is equivalent to:
[source,golo]
----
let tmp1 = [8..20]: __$$_destruct(3, true, array[false, true, false])
let a = tmp: get(0)
let b = tmp: get(2)
let it = list[1, 2, 3, 4, 5]: iterator()
let tmp2 = it: __$$_destruct(4, true, array[true, false, false, true])
let x = it: get(1)
let y = it: get(2)
----

where `tmp1` will be `array[8, null, [10..20]]` and `tmp2` will be `array[null, 2, 3, null]`.


[WARNING]
====
As of version 3.4, the destructuring behavior has changed, particularly when the remainer aspect and when not enough variables are provided.
To use the old-syle behavior, one can use the `&useOldstyleDestruct()` macro to switch locally, or set the `golo.destruct.newstyle` property or
`GOLO_DESTRUCT_NEWSTYLE` environment variable to `true` to switch globally.
====

=== Operators

Golo supports the following <<operators,set of operators>>.
Expand Down
15 changes: 14 additions & 1 deletion src/main/golo/gololang/macros.golo
Original file line number Diff line number Diff line change
Expand Up @@ -133,10 +133,23 @@ See also [`gololang.meta.Annotations::makeDeprecated`](meta/Annotations.html#mak
macro deprecated = |args...| {
# TODO: generate a list of deprecated functions and types (a la javadoc) ?
# TODO: when swithching to java >= 9, adds the `since` and `forRemoval` arguments to the annotation
let positional, named = parseArguments(args)
let positional, named, _ = parseArguments(args)
require(positional: size() > 0, "`deprecated` macro must be applied on an element")
return gololang.meta.Annotations.makeDeprecated(
named: get("since")?: value(),
named: get("comment")?: value(),
positional)
}

----
Use old-style destructuring for the current module.
This macro customize the behavior of the destructuring feature by forcing the use of the `destruct` method instead of
`_$$_destruct`.
This is a toplevel macro.
----
@contextual
macro useOldstyleDestruct = |self| {
self: enclosingModule(): metadata("golo.destruct.newstyle", false)
}
2 changes: 1 addition & 1 deletion src/main/golo/gololang/meta/Annotations.golo
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,7 @@ See [`parseArguments`](../macros/Utils.html#parseArguments_1),
[`getLiteralValue`](../macros/Utils.html#getLiteralValue_1)
----
function extractAnnotationArguments = |annotation, args| {
let p, n = parseArguments(args)
let p, n, _ = parseArguments(args)
let namedMap, unknown = extractAnnotationNamedArguments(annotation, n)
let positionnals, elements = filterPositionnalArguments(p)
let fields = extractAnnotationFields(annotation)
Expand Down
151 changes: 147 additions & 4 deletions src/main/golo/standard-augmentations.golo
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,25 @@ augment java.lang.Iterable {
}
return false
}

----
New style destructuring helper
If a remainer is included, it will be an iterable reusing the underlying iterator.
- *param* `number`: number of variable that will be affected.
- *param* `substruct`: whether the destructuring is complete or should contains a remainer.
- *param* `toSkip`: a boolean array indicating the elements to skip.
- *returns* an array containing the values to assign.
----
function __$$_destruct = |this, number, substruct, toSkip| {
let it = this: iterator()
let d = destructIterator(it, number, substruct, toSkip)
if substruct and not toSkip: get(number - 1) {
d: set(number - 1, asInterfaceInstance(java.lang.Iterable.class, -> it))
}
return d
}
}

# ............................................................................................... #
Expand All @@ -306,9 +325,66 @@ augment java.util.Collection {
----
Destructuration helper.
* return a tuple of the values
- returns a tuple of the values
*Deprecated since 3.4. This method should not be called directly and is no more used by new style destructuring.
----
function destruct = |this| {
org.eclipse.golo.runtime.Warnings.deprecatedElement("destruct", "gololang.StandardAugmentations.Collection")
return Tuple.fromArray(this: toArray())
}

----
New style destructuring helper
If a remainer is included, it will be a collection of the same type.
- *param* `number`: number of variable that will be affected.
- *param* `substruct`: whether the destructuring is complete or should contains a remainer.
- *param* `toSkip`: a boolean array indicating the elements to skip.
- *returns* an array containing the values to assign.
----
function destruct = |this| -> Tuple.fromArray(this: toArray())
function __$$_destruct = |this, number, substruct, toSkip| {
if number < this: size() and not substruct {
throw org.eclipse.golo.runtime.InvalidDestructuringException.notEnoughValues(number, this: size(), substruct)
}
if number == this: size() and not substruct {
return org.eclipse.golo.runtime.ArrayHelper.nullify(this: toArray(), toSkip)
}
let d = newTypedArray(Object.class, number)
let col = match {
when toSkip: get(number - 1) then null
otherwise _newWithSameType(this)
}
d: set(number - 1, col)
if number <= this: size() and substruct {
let it = this: iterator()
for (var i = 0, i < number - 1, i = i + 1) {
if not toSkip: get(i) {
d: set(i, it: next())
} else {
it: next()
}
}
if col isnt null {
while it: hasNext() {
col: add(it: next())
}
}
return d
}
if number == this: size() + 1 and substruct {
var i = 0
foreach v in this {
if not toSkip: get(i) {
d: set(i, v)
}
i = i + 1
}
return d
}
throw org.eclipse.golo.runtime.InvalidDestructuringException.tooManyValues(number)
}

----
Maps a function returning a collection and flatten the result (a.k.a bind)
Expand All @@ -320,7 +396,7 @@ augment java.util.Collection {
- *param* `func`: a mapping function returning a collection
----
function flatMap = |this, func| {
let result = this: newWithSameType()
let result = _newWithSameType(this)
foreach elt in this {
result: addAll(func(elt))
}
Expand Down Expand Up @@ -859,8 +935,30 @@ augment java.util.Map {
augment java.util.Map$Entry {
----
Destructurate a map entry in key and value
*Deprecated since 3.4. This method should not be called directly and is no more used by new style destructuring.
----
function destruct = |this| -> [ this: getKey(), this: getValue() ]
function destruct = |this| {
org.eclipse.golo.runtime.Warnings.deprecatedElement("destruct", "gololang.StandardAugmentations.Map.Entry")
return [ this: getKey(), this: getValue() ]
}

----
New style destructuring helper
The destructuring must be to exactly two values. No remainer syntax is allowed.
- *param* `number`: number of variable that will be affected.
- *param* `substruct`: whether the destructuring is complete or should contains a remainer.
- *param* `toSkip`: a boolean array indicating the elements to skip.
- *returns* an array containing the values to assign.
----
function __$$_destruct = |this, number, substruct, toSkip| {
if (number == 2 and not substruct) {
return array[this: getKey(), this: getValue()]
}
throw org.eclipse.golo.runtime.InvalidDestructuringException("A Map.Entry must destructure to exactly two values")
}

----
Convert then entry into an array containing the key and the value.
Expand Down Expand Up @@ -952,3 +1050,48 @@ augment java.io.BufferedReader {
----
function iterator = |this| -> gololang.IO$LinesIterator.of(this)
}

local function destructIterator = |it, number, substruct, toSkip| {
let d = newTypedArray(Object.class, number)
if substruct and not toSkip: get(number - 1) {
d: set(number - 1, it)
}
let nbValues = match {
when substruct then number - 1
otherwise number
}
try {
for (var i = 0, i < nbValues, i = i + 1) {
if toSkip: get(i) {
it: next()
} else {
d: set(i, it: next())
}
}
} catch (e) {
if e oftype java.util.NoSuchElementException.class {
throw org.eclipse.golo.runtime.InvalidDestructuringException.notEnoughValues(number, substruct)
}
throw e
}
if (it: hasNext() and not substruct) {
throw org.eclipse.golo.runtime.InvalidDestructuringException.tooManyValues(number)
}
return d
}


augment java.util.Iterator {

----
New style destructuring helper
If a remainer is included, it will contain the iterator itself.
- *param* `number`: number of variable that will be affected.
- *param* `substruct`: whether the destructuring is complete or should contains a remainer.
- *param* `toSkip`: a boolean array indicating the elements to skip.
- *returns* an array containing the values to assign.
----
function __$$_destruct = |this, number, substruct, toSkip| -> destructIterator(this, number, substruct, toSkip)
}
18 changes: 10 additions & 8 deletions src/main/java/gololang/AbstractRange.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import java.util.AbstractCollection;
import java.util.Iterator;
import java.util.Arrays;
import java.util.Objects;

import static java.util.Objects.requireNonNull;

Expand Down Expand Up @@ -110,18 +111,14 @@ public boolean equals(Object other) {
}
@SuppressWarnings("rawtypes")
Range otherRange = (Range) other;
return this.from().equals(otherRange.from())
&& this.to().equals(otherRange.to())
&& this.increment() == otherRange.increment();
return this.from.equals(otherRange.from())
&& this.to.equals(otherRange.to())
&& this.increment == otherRange.increment();
}

@Override
public int hashCode() {
return Arrays.hashCode(new int[]{
this.from().hashCode(),
this.to().hashCode(),
this.increment()}
);
return Objects.hash(this.from, this.to, this.increment);
}

@Override
Expand All @@ -137,6 +134,11 @@ public boolean isEmpty() {
return from.compareTo(to) >= 0;
}

/**
* Destructuring helper.
* @deprecated This method should not be called directly and is no more used by new style destructuring.
*/
@Deprecated
public Tuple destruct() {
Object[] data = new Object[this.size()];
int i = 0;
Expand Down
7 changes: 7 additions & 0 deletions src/main/java/gololang/BigIntegerRange.java
Original file line number Diff line number Diff line change
Expand Up @@ -104,5 +104,12 @@ public BigInteger next() {
};
}

/**
* {@inheritDoc}
*/
@Override
public Range<BigInteger> newStartingFrom(BigInteger newStart) {
return new BigIntegerRange(newStart, this.to()).incrementBy(this.increment());
}
}

8 changes: 8 additions & 0 deletions src/main/java/gololang/CharRange.java
Original file line number Diff line number Diff line change
Expand Up @@ -95,4 +95,12 @@ public Character next() {
}
};
}

/**
* {@inheritDoc}
*/
@Override
public Range<Character> newStartingFrom(Character newStart) {
return new CharRange(newStart, this.to()).incrementBy(this.increment());
}
}
Loading