Println (or Log) in Kotlin - Standard / Alternative Methods - Part 2 of 2
Println? Part 2
In Part 2
Ready!
Again, I want you to think about our previous alsoPrintln template. Are there any improvements we can make to that? There are a few solutions for this, but not all have any real benefit. Don't worry about the order of our solutions.
This was our previous solution:
fun <T> T.alsoPrintln(): T {
println(this)
return this
}
Click to reveal
First, we could inline it.
inline fun <T> T.alsoPrintln(): T {
println(this.toString())
return this
}
But I think the general advice would be not to inline this function.
There are a few reasons not to inline here:
- No lambda parameter
- Increases code size
- JVM is good at automatically applying inline where appropriate
If you are very interested in this, I would suggest trying it out with some highly accurate benchmarks. I think I'd recommend against. This is actually not as easy as I thought to clearly dismiss.
Click to reveal
fun <T : Any> T.alsoPrintln(): T {
println(this)
return this
}
Click to reveal
We can now rename our original template version with Nullable.
fun <T> T.alsoPrintlnNullable(): T {
println(this.toString())
return this
}
If you didn't catch it, this change highlights why we might want to add Any to the original template. T : Any will not accept nulls and so having a specific version to explicitly accept null values has few advantages (explained later).
Getting Harder
Okay, this is becoming quite challenging now! We'll make three improvements to the previous function.
Consider these points:
- reducing the length
- a better way to handle a null result
- any calls we don't need?
Click to reveal
We can reduce this to one line (three lines to fit in the blog mobile format) and we can also allow the user to specify a custom String if they get a null result.
fun <T> T.alsoPrintlnNullable(
valueIfNull: String = "null <T>"
): T = also { println(it ?: valueIfNull) }
Reducing the function to a single line using = also { } is a very nice improvement. We will use that style from now on. This acts exactly the same as the longer function so still allows for call chaining. Another point we'll apply to all future versions: the previous function used toString() which we do not need. This is automatically taken care of by the println function.
This is also a good chance to think about some of the advantages of a nullable version. The first, obvious, advantage is the error message. The second is reading the code. When you see alsoPrintlnNullable, you instantly know the function could return a null result. Handling nulls with alsoPrintln may hide this detail from the user and make code harder to understand.
Numbers
This is looking great. Let's return to working with numbers next.
We previously wrote toString functions to deal with Floats and Double that handled rounding. Those functions allowed the user to easily format the String.
I'll put the refactored single line version (3 for the post) of those below.
fun Double.toRoundedString(
decimalPlaces: Int = 2
) = "%.${decimalPlaces}f".format(this)
fun Float.toRoundedString(
decimalPlaces: Int = 2
) = "%.${decimalPlaces}f".format(this)
What about println handling for Int, Long, Short, and Byte? Ahh, we also have the U versions (non-negative) of those to consider.
Click to reveal
You may have got a little over excited here remembering you could extend Number and come up with this:
fun Number.alsoPrintln() =
also { println(it) }
fun Number?.alsoPrintlnNullable(
ifNull: String = "null Number"
) = also { println(it ?: ifNull) }
At first glance, this is great. You have nice short clean code that is very clear.
But we're forgetting three points.
- First, all of these cases are already covered by our templated version above.
- Secondly, it's unlikely we ever really need to just print a single number to the console without any real context.
- Finally, the function returns a Number and not specific number type applied to.
needsLong(getLongValue().alsoPrintln())
A function that requires a Long will no longer work and Android Studio will report the following: "Argument type mismatch: actual type is 'Number', but 'Long' was expected."
Definitely avoid this one as it doesn't return the type. The real solution is just to use the <T> templates above if you really want a single number printed without context.
I'll rate this 1/10 for practicality (even if it initially seems like a good solution).
Any?
Click to reveal
Point of Interest
alsoPrintln()
PointsOfInterest.alsoPrintln()
One Last Trap
Click to reveal
val myArray = arrayOf(1,2,3,4,5)
myArray.alsoPrintln()
// returns [Ljava.lang.Integer;@6d7b4f4c
println("myArray: $myArray")
fun <T: Any> Array<T>.alsoPrintln() =
also {
println(it.contentToString())
}
println("This is my array:\n" +
" ${myArray.contentToString()}")
Wrapping Up
The Best
private fun String.alsoPrintln()
= also { println(it) }
private fun String?.alsoPrintlnNullable(
ifNull: String = "null String"
) = also { println(it ?: ifNull) }
private fun String.alsoLogV(
tag: String
) = also { Log.v(tag, this) }
private fun String?.alsoLogVNullable(
tag: String, ifNull: String = "null String"
) = also { Log.v(tag, it ?: ifNull) }
private fun <T : Any> T.alsoPrintln() =
also { println(it) }
private fun <T> T.alsoPrintlnNullable(
valueIfNull: String = "null <T>"
) = also { println(it ?: valueIfNull) }
private fun Double.toRoundedString(
decimalPlaces: Int = 2
) = "%.${decimalPlaces}f".format(this)
private fun Float.toRoundedString(
decimalPlaces: Int = 2
) = "%.${decimalPlaces}f".format(this)
The Niche Versions
private fun Number.alsoPrintln() =
also { println(it) }
private fun Number?
.alsoPrintlnNullable(
ifNull: String = "null Number"
) = also { println(it ?: ifNull) }
private fun <T: Any> Array<T>.alsoPrintln() =
also { println(it.contentToString()) }
Test Values
private fun getStringValue() =
"Stringy"
private fun getNullString():
String? = null
private fun getDoubleValue() =
1234567.1234567890
private fun getFloatValue() =
123.4567f
private fun getLongValue() =
1234123134893489033L
private fun getNullLong():
Long? = null
private fun getNullBoolean():
Boolean? = null
private fun getArray() =
arrayOf(1,2,3,4,5)
Summaries
Part 1 Summary
- Three standard approaches to printing function return values, ranging from verbose variable storage to the idiomatic .also { println(it) } pattern
- Alternative extension functions like String.println() and the clearer String.alsoPrintln() for convenience in testing and prototyping
- Wrapper functions as another approach to combine function execution with printing
- Number formatting extensions for Double and Float with customizable decimal places, leading to the useful toRoundedString() function
- Template/generic versions using <T> for any type, though with limited practical value for primitives without context
- Call chaining benefits demonstrated through extension functions that return this
- Logging extensions following the same pattern as alsoPrintln for Android's Log.v() and other log levels
Part 2 Summary
- Function refinement through single-line expression syntax using = also { } for cleaner, more concise code
- Nullable handling with separate alsoPrintlnNullable() functions that accept custom messages for null values
- Type constraints using <T : Any> vs <T> to distinguish between nullable and non-nullable versions
- Pitfalls with Number and Array extensions that return base types instead of specific types, causing type mismatch issues
- Array toString() gotcha requiring contentToString() instead of the default toString() for readable output
- Inline function considerations and when not to use them (no lambda parameters, code size concerns)
- Best practices collection providing the final, production-ready versions of all explored functions
Conclusion
I feel I may have tricked you a little with the Println title as that's not really what this post is all about.
My main goal here was to make you think about how you can make best use of some of the simple but powerful Kotlin features that make our lives as programmers easier. I wanted to play with different types of solutions and highlight the advantages and pitfalls.
This was originally intended as a short blog entry, but I expanded the scope significantly while constantly refining the functions and testing different ideas.
I hope you found this as interesting as me and learned something useful! I know I did.
Links
- Full Code Listing with Console Output (Gist)
- Part 1
- Raw Kotlin Posts
If you are interested in raw Kotlin (which I used for everything in this post), you may also want to check out these posts:
- Faster Backend Development: Running 'Raw' Kotlin in Android Studio - Set up your Android Studio to run Kotlin without Android
- Plain Kotlin Android Style Logging - Logging with Log so you can easily move code back and forth into and Android environment. I used this for the Log.v function.
Comments
Post a Comment