https://kotlinlang.org logo
#feed
Title
l

louiscad

09/20/2021, 3:18 PM
Just published my first article, I hope you like it! https://blog.louiscad.com/how-to-return-2-values-with-0-allocation-in-kotlin
Fun
fact: I took over 50 photos before I settled for the cover picture
blob upside down
👍🏼 2
👍 13
e

elect

09/20/2021, 4:14 PM
really nice, kudos Louis
s

Stephan Schroeder

09/21/2021, 1:32 PM
1. clever solution 👍 2. probably too clever 😐 It's really hard to understand this code 😔 Just use a
Pair
if you want to return to different types
Copy code
fun getNameAndAge(): Pair<String, Int> = "Bob" to 42
val (name: String, age: Int) = getNameAndAge() // no danger of accidentally mixing up the order since it'd be a compiletime error
if the two types are the same, there's a danger of not knowing which is which
Copy code
getUserNameAndPassword(): Pair<String, String> = "bob79" to "p4ssw0rd"
val (password, username) = getUserNameAndPassword() // oh no, wrong order
In that case either use a
data class
but it's still possible to mess up order when using destructoring
Copy code
class UsernamePassword(val username: String, val password: String)
getUserNameAndPassword() = UsernamePassword(
  username="bob79",
  password="p4ssw0rd",
)
val usernamePassword = getUserNameAndPassword()
val (password, username) = usernamePassword // oh no still wrong
// though long for is way less likely to be mistaken
val username = usernamePassword.username
val password = usernamePassword.password // way less likely to mess up
or you might use
@JvmInline value class
to generate typesafe zero cost wrappers
Copy code
@JvmInline value class Username(val value: String)
@JvmInline value class Password(val value: String)
fun getUserNameAndPassword(): Pair<Username, Password> = Username("Bob") to Password("p4ssw0rd")

val (username: Username, password: Password) = getUserNameAndPassword() // impossible to confuse (would be a compiletime error), zero overhead for the wrappers, single allocation of Pair, very readable
Readability basically always >> minimization of allocations
e

elect

09/21/2021, 2:14 PM
Readability basically always >> minimization of allocations
Not always, sorry. Anyway, it will be nice if Kotlin could provide a language construct for that
2
l

louiscad

09/21/2021, 5:28 PM
I personally don't find it that hard to understand the code, but I am clearly biased. Just so you know, I don't use that trick often, but I found it helpful for the use case you can see at the end of the article. Readability is important, this article is not questioning that.
About your "basically always" statement, I simply cannot agree, we're surrounded by resource constrained software (especially embedded systems). I already covered the matter in the article for code run at high frequencies.
s

Stephan Schroeder

09/21/2021, 9:31 PM
on embedded systems use Rust or Zig. Kotlin is a fantastic hammer, but not as good a screwdriver.
l

louiscad

09/21/2021, 9:44 PM
Then you have mobile devices, which in a way are a kind of embedded system. There's also wearables. Anyway, I've seen enough slow and janky apps to know that being mindful about resources, especially for hotspots, is important.
y

Youssef Shoaib [MOD]

10/04/2021, 1:37 PM
The lovely thing about this trick is that it can sort of be extended to provide even more power if the compiler had the ability to inline lambdas that are returned by inline functions. For example, the example in your article would be:
Copy code
fun main(){
    val (stuff, bonus) = getStuff()
}

inline fun getStuff(): ZeroCostPair<Stuff, Bonus> {
    val stuff = grabStuffFromCargoBikeBasket()
    val bonus = inspirationElixir()
    return ZeroCostPair(stuff, bonus)
}

//Implementation details
typealias ZeroCostPair<F, S> = (PairCall, F?, S?) -> Any?

enum class PairCall {
  First,
  Second
}

// Mimicking a constructor for the type.
inline fun <F, S> ZeroCostPair(first: F, second: S): ZeroCostPair<F, S> =
    { call, _, _ -> 
        when (call) {
          PairCall.First -> first
          PairCall.Second -> second
        }
    }

// Again, the parameters are useless, so just pass in null for them since during runtime the JVM won't actually know what
// F and S are since they get erased.
// We can safely cast the result of invoking the function as F or S because we know that ZeroCostPairs created using
// the factory function always follow the pattern of returning an F if PairCall.First is passed in and likewise for S.
inline val <F, S> ZeroCostPair<F, S>.first get() = this(PairCall.First, null, null) as F
inline val <F, S> ZeroCostPair<F, S>.second get() = this(PairCall.Second, null, null) as S
And the idea is that the returned lambda would just be fully inlined at compile time. I currently have a ticket for this and a prototype compiler plugin
👀 1
4 Views