Kotlin support
When using EC for reducing garbage, a dedicated "kotlin support" module could offer inline methods for things like iterating primitive collections. When using these methods from kotlin code, you could .forEach an IntList without the overhead of allocating an IntProcedure. Internally, this forEach would be implemented as a simple indexed loop over the list.
Kotlin does have extension methods, so implementing this as a separate module would not be an issue. However, there remain name clashes with existing methods like forEach(IntProcedure). This might make a new name necessary for these kotlin-specific methods.
Additionally, with a good JIT the inlining should happen anyway. I'm not certain how much benefit there would really be, so that will require some microbenchmarking.
@yawkat if you want to try this out, I will be extremely supportive. Go for it! From a release and separation perspective, we can create a separate module just for kotlin support. I will be happy to create milestone releases if you need them to test and benchmark. Then we can take a call if we want to keep the separate module or do something else. Thoughts?
Sounds good to me. I'm doing some preliminary testing on which methods are useful now. I will make a work-in-progress PR with the module structure you mentioned as soon as I have some of the extensions prepared.
I am also now thinking more in terms of "convenience" methods to make the collections behave more like kotlin collections, instead of low garbage specifically.
Okay, I've thought a bit more about this. There are three kinds of methods that could be added for kotlin to make decisions on. Note that all this is primarily with the primitive collections in mind because this is the part of EC I work with the most. Some points may apply to object collections, others may not.
operator functions
These are functions like component1..5 to allow destructuring declarations, and functions like plus. Adding these is a no-brainer. Some of these methods can be mapped very easily to existing methods (component1 is just get(0)), but others are more complicated. As an example, there is plus which would concatenate two lists. For Immutable*Collection we can simply map these to the existing newWith / newWithAll methods.
However, unlike kotlin, EC has three interfaces (Mutable, Immutable and the common base interface) while kotlin only has two (base interface and mutable). In kotlin, plus is declared for the base interface, but in EC newWith only exists for the immutable interface. The first question is: should we declare plus for the base interface like in the kotlin stdlib, or should we stick it on the Immutable versions only? And if we stick it on the base class, what implementation do we use to actually create the new collection? Kotlin simply uses ArrayList.
The second question is what to do with mutable interfaces. In kotlin, there is an extension function MutableCollection.plusAssign which is equivalent to addAll:
val l1 = ArrayList<String>()
val l2 = l1
l1 += "foo"
assert(l2[0] == "foo")
(This is similar to the behavior of python +=)
I am not a fan of this behavior in kotlin because it makes a += b behave differently to a = a + b which is just weird. However, this could be implemented for Mutable*List as well.
In my opinion, we should stick to implementing the plus operator (and friends) on Immutable*Collection and leave the other collection types alone for now. We can always add the other operators later, but as it is right now I don't really see a big advantage towards implementing them.
Better performance using inlining
This was the original motivation for this issue. In particular, kotlin allows declaring "functional" extensions such as forEach that will be inlined at compile time and transformed into an iterator-based loop. This has the potential advantage of removing one lambda allocation and the associated overhead (virtual call). However, my preliminary testing has shown that the JVM is actually so good at inlining the normal java-lambda-based methods that I am not sure whether this would actually bring a performance advantage.
There is another small point to this: Inline functions let the user do things like early returns inside the forEach. I don't think this is a big enough advantage to warrant implementing these functions, though.
Convenience functions
This is a harder decision. Kotlin offers various convenience methods on the collection classes (Iterable, Collection, List...) to augment the sparse java stdlib collections. These extension functions like map, single or join are pretty much "in competition" with similar methods on the EC object collection classes. There are two choices to make:
- Should these functions be implemented as extension functions in the kotlin module, or should we just go ahead and add them to the standard primitive collections? There isn't really anything kotlin-specific about them after all, and the primitive collections are lacking in API compared to the object collections anyway.
- What naming to use? Kotlin uses the common jvm names for these functions (
map,flatMap,filter...) but EC has traditionally used names from smalltalk (collect,flatCollect,select...). If we add these functions to primitive collections, do we want to emulate the kotlin collections or do we want to emulate the EC object collections?
Since my main exposure to EC is through primitive collections and I use the normal stdlib for object collections my tendency is to go to jvm naming for these functions (map) and to avoid inconsistencies in EC stick them into the kotlin module as extension functions. However this is another one of those more difficult questions so I'm also partial to not implementing these convenience functions at all.
I would love to hear feedback on these points, since these decisions appear pretty major at least within the scope of a EC kotlin module.
I have renamed the issue to more accurately reflect the work I'm actually doing here now.
Hi @yawkat I assigned the issue to you. Thank you for working on this!
I'd still like some feedback on the design choices listed above - I'm not certain what the best route is.
I'll take an in depth look today and give you some feedback.
@yawkat can you start with Immutable collections for the operator functions, and we can see if it makes sense then to move it up higher to support the Readable and Mutable interfaces later?
We can hold off on plusAssign on Mutable interfaces to start. I'm a fan of starting small and building up functionality over time, or as we have specific use cases for it. We already have the with/withAll methods on MutableCollection which are probably more useful than plusAssign anyway.
On the naming of the convenience functions, I could go either way. The question I would ask is that if we stick with Kotlin/Java names, does it make it easier to convert between Kotlin standards collections and EC Collections. Similarly, if we stick with EC standard names, does it make it easier to convert from Java EC projects to Kotlin EC projects? I'd like to hear your thoughts on this as you are much more familiar with the space and possibilities.
On the method forEach override issue, we have a second method each that we added back before we converted EC to Java 8. Not sure if this will help.
I'm late to this thread but I'd love to hear more about the operators like + and +=. Over the years we've taken some inspiration from Scala. Their collection framework also has 3 interfaces (readable, mutable, immutable). They have operator methods, but + and += manage to feel consistent across the types.