Idiomatic approach to `apply` with varargs

At feat: Add Source#elements by He-Pin · Pull Request #2429 · apache/pekko · GitHub (pekko-streams) we are discussing adding a varargs constructor to a Source (which is a pure stream), i.e.

def apply[T](elements: T*): Source[T, NotUsed]

With this you can use very concise syntax to create a Source i.e. Source(1,2,3). The issue is that using this syntax has some problems, i.e. if you do something like Source(List(1,2,3),List(4,5,6)) it will just create a Source that emits 2 elements, each one being an List (one containing 1,2,3 and the other 4,5,6) where as doing Source(List(1,2,3)) will construct a Source that just emits 1,2,3 as distinct elements (pekko-streams already has an existing apply for immutable collections)

Essentially this means we are kind of in a battle of consistency in passed arguments (i.e. whatever is passed in must be emitted as the exact same element) versus actually providing a benefit to end users in terms of concise syntax for constructing a Source. For example, originally in that PR the proposal was to have a specific non-apply method for varargs, i.e.

def elements[T](elements: T*): Source[T, NotUsed]

However such a method is kind of pointless given that Source.elements(1,2,3) is even longer than Source(List(1,2,3)) so its not providing much benefit.

So question is, what really is the Scala idiomatic way of handling this issue and how do other libraries/community design with this? I am personally of the view that varargs in general are not even being used that often in Scala and so there is merit in not even bothering to add support for it in our scaladsl (its still being added to our javadsl but thats separate).

1 Like

@mdedetrich is this something that would be fixed by SIP-70 - Flexible Varargs by lihaoyi · Pull Request #105 · scala/improvement-proposals · GitHub letting you use * to unpack each list?

I am afraid this is a design choice you have to make and I am not sure what on the language level can help you. I would say having overloaded apply with different semantics is surprising and I would avoid that - and your examples show why.

I would still argue it is better readable (less nesting), but if length is what you are after, you can use something like Source.of. I would avoid Source.from, as that already is used in stdlib to provide funcionality like you current apply does. If designing from scratch, I would use Source.from to create from lists, and Source(varargs*) to create from list of elements, as this closely maches what stdlib does.

1 Like

This I am aware of, I am trying to figure out what other Scala standard library designers do with this predicament

We are not designing from scratch, so changing the current apply that we have for immutable collections is not going to happen as it would break users. Source.of sounds like a decent compromise, although Source.elements is more precise in terms of language articulation (of can mean of anything where as with elements we are talking about distinct separate elements).

I will have a read of this as it does seem related however this is not helpful to Apache Pekko as we are dealing with Scala 2/Scala 3.3 LTS

To me even this sounds better readable (and easier to type) than Source(List(1, 2, 3)). Character count often does not seem like a good measure of code complexity. Longer identifiers are sometimes easier to understand than short ones, and I think it is very rare one’s productivity is bound by a typing speed (esp. in the age of autocomplete).

Therefore if you think this carries the meaning best, I would stick with it.

2 Likes

Based on most of the libraries I use I’d say the most idiomatic is an API like this:

def apply[T](elements: T*): Source[T, NotUsed]
def fromIterable[T](elements: immutable.Iterable[T]): Source[T, NotUsed]
2 Likes

Yes, but here is an apply[T](array:Array[T])` there.

I would suggest Source.items(elements: T*) if you need varargs at all. It is a tiny bit shorter than Source(List(1, 2, 3)) and is clearer.

Personally I mostly avoid varargs, though.

1 Like

Great suggestion.

Multi.createFrom().items(1, 2, 3, 4, 5)
        .onItem().transform(i -> i * 2)
        .select().first(3)
        .onFailure().recoverWithItem(0)
        .subscribe().with(System.out::println);

https://smallrye.io/smallrye-mutiny/latest/tutorials/creating-multi-pipelines/#the-multi-type