What is the number of overloads to a single functi...
# library-development
c
What is the number of overloads to a single function that is simply too many? I have a case where I would need ~600 overloads for a single function, and that sounds like a bad idea.
😆 7
j
Honestly I don't know what to tell you but I'm really curious about why on earth you would need 600 overloads 😂
âž• 9
a
I suspect the threshold might have been a couple hundred ago!
😂 3
d
I'm assuming that you're doing some sort of code generation to end up with so many overloads. If any of those overloads delegate to others providing default values, then I would recommend combining them into fewer functions that have default arguments instead.
z
Take a lesson from the JVM Function implementation and stop at 22 😄
d
I think that 600 overloads for a single function would introduce a bunch of issues: • Information overload for users of the library when IntelliJ shows so many options to choose from • It might affect IntelliJ performance during the auto-complete suggestion process • Increased compile times • Increased package / dependency size. Some libraries are avoided due to this reason. • Increased class loading time • Reduced CPU instruction cache utilization affecting performance when many functions are used. • Android has a 64K function limit. This is mostly a non-issue as unused functions get stripped out at compile time but if your library actually ends up using many of these then it would be concerning. If just a single function has 600 overloads, the entire library could be huge. • etc. I would consider some different approaches: • Trade type safety for runtime safety to allow generic types that get resolved at runtime • Only define overloads for the most common types and provide utilities for converting other types into the common ones. This should dramatically reduce the combinatorial explosion that you're experiencing. • Or a combinations of the above where you have explicit overloads for the common types and a generic function that handles the other types at runtime.
d
If I understood the general problem correctly (granted, I didn't dive in too deep), then my impression is that extra verbose conversion operators could be a good thing in your use case (if not in your case, then at least in some visually similar ones).
Copy code
User::score set cond { User::role eq User::candidate }
    .then { 1 }
    .else { User::score add 1 }
is certainly readable, no question. However, there is too high of a potential to misunderstand the API (by being unattentive) or just mistype it:
Copy code
user.score set cond { user.role eq user.candidate }
    .then { 1 }
    .else { user.score add 1 }
It seems these will have completely different behaviors, right? If so, the verbosity could be a good thing if instead of
of
, some name is chosen that accurately explains what will happen. Instead of relying on a bunch of easily ignored dots and colons to distinguish the behaviors, you'd have your DSL tell the whole story. Maybe it would also be worth splitting the DSL into several separate ones: one that would only work with
T
, the other would only work with
Field<T>
, etc. Depends on if it's reasonable to mix-and-match several types of sources of data, or if it indicates a mistake.
c
>
.else { User::score add 1 }
>
.else { user.score add 1 }
> It seems these will have completely different behaviors, right? That's correct. The first one adds 1 to the value of the field
score
as it is stored in the database, whereas the second one adds 1 to the regular Kotlin
score
variable. Indeed, the DSL could be modified to be:
Copy code
.else { field(User::score) add of(1) }
and
Copy code
.else { of(user.score) add of(1) }
which wouldn't compile for the incorrect case. However, I don't really know how much of a possible error that is. If the user had fields
minScore
and
maxScore
, they could also type the wrong field name and assign the wrong field, but I feel like that's a basic programming problem, the user could always refer to the wrong field. I'd have to do user-research to know if it's a mistake that actually happens or not. > Maybe it would also be worth splitting the DSL into several separate ones: one that would only work with
T
, the other would only work with
Field<T>
, etc. Depends on if it's reasonable to mix-and-match several types of sources of data, or if it indicates a mistake. In this case, there really is a single DSL. This is a DSL for MongoDB, and MongoDB is typed dynamically, so in "real" requests you can put whatever there. In my DSL, I encode the different kinds of things you can put there as different types. Ideally, I'd have a supertype of "things you can put in a DSL", but two of the four things you can put there (
KProperty1
and "just a regular Kotlin variable") can't be part of custom type hierarchies at the moment. With unions (or "unions-as-an-implicit-conversion" that I mentioned in one of the threads above), I could create such a supertype, and thus the entire reason for having overloads goes away.
d
they could also type the wrong field name and assign the wrong field
The main differences are that • `minScore`/`maxScore` copy-paste errors are indeed unavoidable, but as the library designer, you can avoid introducing the same kind of issue to your DSL! No need to give up just because the world isn't perfect. • You can always detect `minScore`/`maxScore` by reading your code carefully. If you don't fully understand how the DSL works, no amount of re-reading your code will help, you'll need to refer to the documentation/Stack Overflow.
c
I get that, but the current situation is still far from ideal. It is already difficult to convince users to type-safe DSLs, especially for databases, because they tend to be overly restrictive and verbose, that I'd really like to avoid needing to add
of()
in every single usage. Though that's my current approach because there aren't any alternatives at the moment.
l
Can't type parameters help you?
c
No, I'm already using them… The four options for each parameter are: •
T
•
KProperty1<*, T>
•
Field<*, T>
•
Value<*, T>
I could create a common interface between
Field<*, T>
and
Value<*, T>
but it makes a few things more complex. However, I can't create a supertype to
Any
, which would be required to have
T
be part of the hierarchy