:kotlin_emoji: Hey everyone! We’ve just released o...
# library-development
a
K Hey everyone! We’ve just released our new library creators' guidelines, which are packed with tips for simplifying your code, keeping things backward compatible, and writing great documentation. Check it out: ➡️ kotl.in/5kc99z
K 11
👍 1
c
Is there a TL;DR of what changed? I'm skimming through it but I don't see any major changes
a
Hi Ivan! 👋 The guide got a revamp to be more user-friendly. Key updates focus on simplifying code, maintaining backward compatibility, and a new section all about writing great documentation. Hope this clears things up! 😊
c
Thanks!
m
Curious why “library creators” vs previously “library authors”? “authors” feels a bit more inclusive to me as “creating” has some “I initially created this library” vibes that “author” doesn’t.
l
Maybe "library makers" could also be an option?
m
I like makers 🙂
c
I prefer "authors", but 🤷
m
I am
authors >= makers > creators
🙂 “makers” has some hardware vibes to me. It’s cool but “authors” might be a bit more mainstream
2
z
We heard the feedback and we'll rename it to "Library authors' guidelines" soon 👍
👍 2
thank you color 2
l
If we have creators, we might as well have readers, updaters, and deleters, and also "editors" to have the complete CRUD(E) librarian 🙃
😂 4
c
I'm definitely more of a library deleter 😅
l
Well, that's nice if you can do it without breaking people's projects!
f
I personally think that the instructions on extensions lack detail. I'm well aware of Roman's Extension-oriented Design essay on the matter, but everything he describes here are extensions for interfaces and thus an improved experience with interface segregation. When it comes to concrete classes (including abstract base classes), the situation is a little more complicated. As a library author, I should choose whether to use an extension based on whether I want to allow users of my library to overload the functionality with their own variation of the same extension or not. Having an extension means that it can be ignored; only the import will tell anyone reading the code which version of a function is actually being used. Rust solves this by requiring ambiguity resolution at the use-site, which could be an approach, but it obviously reduces ergonomics. Having a member means that it's impossible for anyone else to provide their own due to shadowing rules. Observations on my side when it comes to extensions also hint at them promoting anemic designs. This is definitely more of a concern for applications than libraries, but something to consider as well with the given instructions. There's also the matter of ergonomics: • IntelliJ (and I guess Fleet is the same) is weak when it comes to auto-completion of extensions. • Kotlin itself can make use of extension functions heavily since they're part of the prelude. However, users of things that aren't in the prelude end up with an endless amount of imports. Roman recommends using star-imports, but everyone was taught not to do this and it's being enforced by pretty much every linter out there (also those common for Kotlin). In my opinion, Kotlin should prelude all extension functions for a class or interface that are defined in the same file as the class or interface to improve ergonomics. Obviously, it would need to be smart about the usage of generics here, since extensions are often used with smart generic bounds to create conditional extensions. • Extensions cannot be used in every context where normal functions can be used. Maybe that changes with K2. Maybe that's only about extensions that aren't top-level defined (I don't have the rules for this at hand). But given that there are differences, it's also important to consider. • I know we're going to get static at some point, but the whole companion problem for static extensions is also a big issue. Not the same -- but it feels related -- is the whole inconsistency surrounding static factories having different names than their types. This always needs to be taught to people who're new to Kotlin. Some learn those that are part of the standard library by heart and remember to try auto-completion with a lowercase character as well to find what they need. Others are just confused. Just to make it very clear, it's
List
vs
listOf
that I'm talking about. The companion operator invoke-pattern as well as having a top-level function with the same name as the type pattern are used by many and it reduces the mental burden on users. I feel that this is especially important for library authors who cannot count on the same massive user base as the standard library can. I'm posting this to the channel as well so that the discussion on this can continue in a dedicated thread. Just in case someone wants to actually discuss any of what I wrote.
👀 3
m
Cool writeup! I'm personally more and more "team extensions" with a few caveats (Java compat) but agree there is definitely a discussion to be had. Might be worth opening a PR in the repo to gather more attention?
Re anemic design, I don't see that too problematic. Business logic and validation being inside extensions doesn't really make them part of the program. I see extension as fully part of the API/lib
f
I'm not part of any team, just a critical thinker. I enjoy extensions and use them, but I also see them misused. Take the anemic with a grain of salt, as I wrote, it's more an application than library problem to begin with. The observation is that people have the goal of “keeping a class empty” as their most important goal. Hence, no encapsulation, no behavior, no validation, … types end up not representing anything of interest and just keeping the original string would have served the same purpose. The only thing the new type brings to the table is scoping of extensions … 😢
👍 1
m
I'd argue that types bring structured data in addition to the scoping of extension. Classes contain data, extensions contain logic. I kind of like the separation, especially in an immutable world.
f
Structure for data alone isn't good enough. Types need to enforce invariants so that we can trust the data within. Unless you include this meaning in structured. That's the what the whole anemic point is about, not enforcing invariants and thus making types pure structs.
👍 1
m
Looks like we need structs in Kotlin :-), maybe Records?
c
You mean value classes? 👀
(read the KEEP, that's exactly what it describes)
m
Value classes can only have one field, IIRC?
c
Yes, currently
👀 1
The KEEP adds multi-field value classes, deep copy, compatibility with reactive frameworks like Compose, etc
👍 1
AFAIK it's on hold until Valhalla lands
👍 2
f
Value classes aren't the equivalent of structs in languages that have them (in their true meaning), and a high-level language like Kotlin doesn't require them. Structs define memory layout (including alignment and padding) and aren't necessarily providing type safety, which might be intentional for FFI purposes. Value classes, on the other hand, are about enforcing invariants in a more lightweight manner than is possible today. Valhalla will go even further by eliminating the need for primitive specialization (at least, that's the plan, and I hope it will be achieved). A value class like
value class SomeValue(val value: String)
can be useful despite being anemic. The problem arises if users add all functionality externally without utilizing the fact that
SomeValue
already represents what they need. For instance, it could enforce non-empty strings:
value class SomeValue(val value: String) { init { require(value.isNotEmpty()) } }
. It could also have a method like
length
that returns the codepoint count instead of the default LATIN-1/UTF-16 char count. Using an extension for this means others can overload
length
with their interpretation, making it unclear what
length
represents without checking the imports.
👍 1
m
>
value class NonEmptyString(val value: String) { init { require(value.isNotEmpty()) } }
I’m onboard with that, I read the guide as
non-empty
is part of the “core” functionality of the class (so it’s fine for that logic to be in the constructor) > have a method like
length
I wouldn’t do this. Or if doing this, I would also make
value
a private implementation detail, e.g.
private val value: String
. Having
length()
as an extension function and the underlying value
public
kind of mixes the contract of that class. Does it represent an UTF-16 string? a sequence of codepoints? something else? Not sure. The good thing is that by making
value
private then it forces
length()
to be method again (and not an extension). Now the problem is defining what is “core” vs “not-core”. And I’m not sure there’s an universal definition there...
f
By adding
private
you do what I advocate for. Proper encapsulation with meaningful behavior. 😊
💯 1
m
Agreed 👍. And I read the guide as OK with that. It’s all in that “core” term. It’s a single word in an otherwise longer article that goes on about extension functions. Maybe a simple improvement to the article could be to add a couple of sentences (and maybe an example) about what “core” means. (Which re-reading your initial message is exactly what you lead with 😃 , i.e. lacking details)
😉 1
s
After 10 years of back-and-forth, I avoid extension wherever possible now. Especially as library author, because discoverability, and imports, suffer. Not allowing users to overload, is not something that I commonly really need or rather value over discoverability, and imports. (Need to read Roman's blog again) I only use extension methods when: • I don't own the type • Need
inline
on interface • Need
reified
on interface • Multiple receivers, extension methods defined inside interfaces, or classes. • Covariance. If but only if adding an additional type parameter isn't possible, or UnsafeVariance is not desired. Namespaces, or module imports, could drastically help here with the import/discoverability issue which could eliminate that problem. It's been a long time ago, but once I worked on a library with an excessive amount of extension functions, and it would obliterate IDEA. Granted, that was a long time ago before Kotlin 1.4 new inference if memory serves me right.
👀 1
c
I genuinely haven't had a single case in the past 3 years or so where IDEA was confused by an extension function. Does that really still happen? The only times I see confusions is when the receiver is a complex type parameter, but those can't be written as member functions anyway.
f
Covariant self I would add to your list, but only if adding an additional type parameter isn't possible, then it's complete.
1
s
I am not talking about confusion @CLOVIS, but performance issues, and discoverability. Yes, I missed that one @Fleshgrinder! Good point.
c
What are the discoverability issues? IDEA autocompletes everything 🤔
s
It needs to scan top-level packages for top-level functions, instead of searching a type for it's members. If you have an enormous amount of packages, with top-level functions, it degrades IDEA performance. Prior to 1.4 it was so problematic, I would get 1-3 seconds delays between key strokes. I'm sure it's much better now, but I have changed my ways since then 😅 I don't see the need so much anymore, except the 5 reasons I listed. EDIT: So I always ask myself now. "Why is this an extension? Can it be a simple member? Why not?"
m
discoverability issues
Compose
getValue
on
by remember {}
comes to mind. Anecdotally, I just copy/pasted a bunch of Ktor code and needed to Command + Enter all the imports (
route
,
get
, etc...) because they weren’t added automatically.
Still like extension functions though
s
Just to be clear I'm not hating on extensions 😄
😄 1
z
For big chunks of copy-pasted Compose code, I've started asking AI Assistant to give me all the imports I need for it to work, it usually gets most 😄
💡 1
e
if there's a lot, I'll add star inports then let the IDE optimize them
f
Even that sometimes doesn't work out. Looking at you Gradle extensions …
m
I just bumped into this discussion (from 5 years ago 🙈 ) ago about star imports and extension functions
e
I don't know if there's some similar design that would work in Kotlin, but Java is adding the ability to import whole modules (consisting of multiple packages) at a time, https://openjdk.org/jeps/476
👀 2