Disclaimer: I am not taking a stance on whether this is okay or not to do, I am only providing examples of how it is possible to achieve the desired syntax.
In Groovy you can get close to the syntax you suggested
recor(whole) { a ->
for (...) {
// ...
if (...) recurse(a[i])
}
}
and in Kotlin you can pretty much do exactly the syntax that you want
recor(a = whole) {
for (...) {
// ...
if (...) recurse(a[i]) // `a` is accessible anywhere in the code block
}
}
except the for
loop syntax, because Kotlin does not have the for
loop variant that you used in your example, but it does have other for
variants that achieve the same, such as for (i in a.indices)
and if index access and list (or array) manipulation would have been a requirement you could always use a while
loop instead.
If anyone knows of other languages where the suggested syntax is possible, please let us know by providing a new answer (or let us know in the comments).
We will explore how to achieve the above in Groovy and Kotlin, but lets start with Javascript and see what syntax limitations prevent it to achieve the desired syntax.
A Javascript (Typescript) example
Here is a full working example in Javascript
const nest123 = [1, 2, 3]
const nest456 = [4, 5, 6]
const nest789 = [7, 8, 9]
const whole = [0, 1, nest123, nest456, nest789]
function arrayExample() {
let cuml = 0
recor(whole, s => {
const a = s.a
for (let i = 0; i < a.length; i++) {
if (typeof(a[i]) == "number") cuml += a[i]
//else if (Array.isArray(a[i])) s.recurse(a[i])
// but no need for the check if we know what to expect
else s.recurse(a[i])
}
})
console.log(cuml) // 46
}
function iteratorExample() {
let cuml = 0
recor(whole, _ => {
for (const v of _.a) {
if (typeof(v) == "number") cuml += v
// else if (Symbol.iterator in v) s.recurse(v)
else _.recurse(v)
}
})
console.log(cuml) // 46
}
// the implementation
// this is a function you would only implement once and then include from some file / library
function recor(a, block) {
const scope = {
a: a,
recurse: a => recor(a, block)
}
// the block call happens here, inside the block calls to recor happen recursively
block(scope)
// here the flow returns to the caller at the current recursion depth
}
arrayExample()
iteratorExample()
In Typescript, since it is a superset of javascript, the same is possible, but as a bonus some type safety could be added. There could be multiple implementations of recor
based on the used type (array vs iterator).
While the above syntax does get close, it has some extra needed syntax (such as =>
) and also has that extra scope parameter, which can be shortened to just a single letter (example with s
above) or even to just an underscore _
, but it still has to be there. Also the block is inside the parenthesis instead of outside.
But can we do better and bring the syntax closer to your example?
A Groovy example
final nest123 = [1, 2, 3]
final nest456 = [4, 5, 6]
final nest789 = [7, 8, 9]
final whole = [0, 1, nest123, nest456, nest789]
def exampleSum(whole) {
int cuml = 0
recor(whole) { a ->
for (int i = 0; i < a.size; i++) {
if (a[i] instanceof Integer) cuml += a[i]
else recurse(a[i])
}
}
println(cuml) // 46
cuml = 0
// doing it with iterators also works
recor(whole) { a ->
for (v in a) {
if (v instanceof Integer) cuml += v
else recurse(v)
}
}
println(cuml) // 46
}
// traverses in depth-first order
def exampleDF(whole) {
recor(whole) { a ->
for (v in a) {
if (v instanceof Integer) println(v)
else recurse(v)
}
}
}
// traverses in breadth-first order
def exampleBF(whole) {
recor(whole) { a ->
def next = []
for (v in a) {
if (v instanceof Integer) println(v)
else next.addAll(v)
}
if (next) recurse(next)
}
}
// the implementation
// this is a function you would only implement once and then include from some file / library
def recor(a, block) {
block.recurse = { next_a -> recor(next_a, block) }
// the block call happens here, inside the block calls to recor happen recursively
block(a)
// here the flow returns to the caller at the current recursion depth
}
exampleSum(whole) // prints the sum of all elements (46) twice
// added 10 and 11 so that DF and BF produce different output
def wholeWithMoreData = whole + [10, 11]
exampleDF(wholeWithMoreData) // prints depth-first traversal
exampleBF(wholeWithMoreData) // prints breadth-first traversal
Now this is much closer and the only difference (apart from is
being instanceof
in Groovy) is the syntax of where the a
variable is "named". In your example you had recor(a = whole) { ... }
, here you have recor(whole) { a -> ... }
. In Groovy you could go with a named argument for a
, but I couldn't find a way to set a
correctly on the block scope to be able to get rid of a ->
as well, but some Groovy gurus out there might be able to do that, which would give you recor(a: whole) { ... }
, where the only difference left would be a: whole
versus a = whole
.
A Kotlin example
Here is a full working example in Kotlin.
val nest123 = listOf(1, 2, 3)
val nest456 = listOf(4, 5, 6)
val nest789 = listOf(7, 8, 9)
val whole = listOf(0, 1, nest123, nest456, nest789)
fun exampleSum(whole: Iterable<*>) {
var cuml = 0
recor(a = whole) {
for (v in a) {
if (v is Int) cuml += v
if (v is Iterable<*>) recurse(v)
}
/*
// if you would need the index as well, you would do
for ((i, v) in a.withIndex()) {
// i is the current index
if (v is Int) cuml += v
if (v is Iterable<*>) recurse(v)
}
*/
// and if you would need total index/list manipulation you would
// convert it to a mutable list and then use a while loop to manipulate it
}
println(cuml) // 46
}
// traverses in depth-first order
fun exampleDF(whole: Iterable<*>) {
recor(a = whole) {
for (v in a) {
if (v is Int) println(v)
if (v is Iterable<*>) recurse(v)
}
}
}
// traverses in breadth-first order
fun exampleBF(whole: Iterable<*>) {
recor(a = whole) {
val next = mutableListOf<Any?>()
for (v in a) {
if (v is Int) println(v)
if (v is Iterable<*>) next.addAll(v)
}
if (next.isNotEmpty()) recurse(next)
}
}
exampleSum(whole) // prints the sum of all elements (46)
// added 10 and 11 so that DF and BF produce different output
val wholeWithMoreData = whole + listOf(10, 11)
exampleDF(wholeWithMoreData) // prints depth-first traversal
exampleBF(wholeWithMoreData) // prints breadth-first traversal
So in Kotlin you could easily achieve what you are asking in you question.
This is the implementation code that would only need to be written once.
interface RecorScope {
/**
* returns the iterable of this [RecorScope]
*/
val a: Iterable<*>
/**
* Calls the block of this [RecorScope] on the given iterable.
*
* @param a the iterable to pass to the block of this [RecorScope]
*/
fun recurse(a: Iterable<*>)
}
fun recor(a: Iterable<*>, block: RecorScope.() -> Unit) {
val scope = object : RecorScope {
override val a = a
override fun recurse(a: Iterable<*>) = recor(a, block)
}
// the block call happens here, inside the block calls to recor happen recursively
scope.block()
// here the flow returns to the caller at the current recursion depth
}
All this is possible to syntactically look how you suggested in your example because of the following features in Kotlin:
- Supports passing functions/lambdas as parameters in function calls, so it supports higher order-functions.
- Supports defining functions at a top-level (as opposed to be limited to defining methods inside classes), which makes it possible to call the
recor
function from anywhere without any (class) prefix
- Supports named arguments, which makes the
a =
part possible in recor(a = whole)
- If the last parameter of a call is a lambda, it can be moved outside the parenthesis, which makes it look like a "control statement with a block" and makes this part possible
recor(...) { ... }
.
- A function literal can be defined (and called) with a receiver (that's how it is called in Kotlin). The receiver then becomes
this
inside the function/lambda scope.
- Calling
this.
can be ommited, which together with 5. makes it possible to write nice DSLs, which allows accessing a
inside the block without the need to write this.a
and it allows calls such as { recurse(...) }
where recurse
receives an additional parameter automatically without the need to pass it at the call site.
- It has smart casts, which means that after checking a value for its type, you don't have to also cast it to that same type, which makes it possible to write
if (v is Int) cuml += v
I think that feature 1. is the important one.
Without features 2. - 7. it is still possible to do what you want, but the syntax would be a little more verbose and the block would have to be inside the parenthesis (like in the javascript example).
If you can use a language that supports higher-order functions and at least some features that are mentioned above, then there is a high chance you can implement something similar and make it look good syntax-wise.
But even if you have to use a language that does not have these features, as long as you can pass functions as parameters to other calls/functions, there is a chance that you can still implement this and make it look somewhat okay syntax-wise.
A C# example
C# only supports feature 1. from the list above (as far as I know) so the closest I could get is this
int cuml = 0;
Recor.with(whole, _ => {
foreach(var v in _.a) {
if (v is int) cuml += (int)v;
if (v is IEnumerable<object>) _.Recurse(v as IEnumerable<object>);
}
});
Console.WriteLine(cuml); // 46
Notice the
- extra prefix required in the starting call because 2. is not possible in C#
- no named argument because 3. is not possible
- lambda is inside parenthesis because 4. is not possible
_
has to be used because 5. and 6. are not possible
- the extra casts are needed because 7. is also not a thing in C# (I used
IEnumerable<T>
instead, but with List<T>
would have been the same).
The C# implementation
static class Recor {
public class Scope {
public IEnumerable<object> a;
private Action<Recor.Scope> block;
public Scope(IEnumerable<object> a, Action<Recor.Scope> block) {
this.a = a;
this.block = block;
}
public void Recurse(IEnumerable<object> a) {
Recor.with(a, this.block);
}
}
public static void with(IEnumerable<object> a, Action<Recor.Scope> block) {
block.Invoke(new Recor.Scope(a, block));
}
}
n
? Whatn
? Wasn+step
supposed to bei+step
?for
…for
loop. (The C-stylefor
loops, which are reallywhile
loops are of course a different beast.) So, given that the two problems are completely different, it is not surprising that the two solutions are also different.