Println (or Log) in Kotlin - Standard / Alternative Methods - Part 2 of 2

Println? Part 2

In Println Part 1, we focused on printing text to consoles using Kotlin idiomatic language features. We had to cut things a little short as it was getting quite long. Let's pick up on where we left off and see how we can further refine and improve our code.

In Part 2

This will go quite a bit deeper into Kotlin language features so newer programmers should see this more as a bit of fun rather than attempt to study this too deeply. More experienced programmers should find this a good test of their abilities.

Note: A lot of the code examples span multiple lines so it is a little more friendly for mobile devices. Sorry, it's a little ugly on desktop. For my more code-heavy posts, I tend to leave the full length versions. At the end, I will link the Gist with much better formatting as well as console output.

Ready!

Great! We can start by making yet another improvement to our alsoPrintln template for Strings.

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.

I'd rate this idea a 5/10. A good thing to think about, but probably not something we need here.

Anything else?
Click to reveal
Yes, we could make to this is using Any. Hope the clue (anything) helped.

fun <T : Any> T.alsoPrintln(): T {
println(this)
return this
}

Can you think of the advantages of doing this? This leads us to the next reveal so please think about that first.
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.

There's actually something else we can improve to make the function shorter but we can look at that later. Congratulations if you already got it!

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?

We previously looked at using T: Any. Why not just use Any? We don't need any code for this response.
Click to reveal

This one is quite simple, if you just use Any, we have the same problem as above with Number. The function returns Any instead of the type that we are operating on.

Point of Interest

What happens if we call this function by itself?

alsoPrintln()

The result is quite interesting.

It returns where we are.

"com.github.d9l9.blogposts.println.PrintlnPart2FunctionsFull$PointsOfInterest@6a2bcfcb"

In this case, PrintlnPart2FunctionsFull and PointsOfInterest are objects. The final @ would be the memory address.

You can see a similar result with the following code (with PointsOfInterest being the name of the object) we are running this on:

PointsOfInterest.alsoPrintln()

These results were also some other interesting results but I'll leave you to discover those yourself from the Gist.

One Last Trap

I said earlier we never need to call toString(). That's 99% true, but it does hide one notable exception.

Let's make this the last reveal...
Click to reveal
Arrays!

val myArray = arrayOf(1,2,3,4,5)
myArray.alsoPrintln()

// returns [Ljava.lang.Integer;@6d7b4f4c
println("myArray: $myArray")

Calling toString() just tells you the type and memory address. It does not format this for you.

We can format this as follows (and create a way to handle this if needed):

fun <T: Any> Array<T>.alsoPrintln() =
also {
println(it.contentToString())
}

Honestly, I don't think this is very useful. It suffers the same problem as the <T> and Number alsoPrintln functions. Without context, it's generally not what you want.

It's far easier just to write it normally:

println("This is my array:\n" +
" ${myArray.contentToString()}")

We'll give the solution a 4/10 but knowing how to handle it is the main point here.

Wrapping Up

That's most of what I want to cover! I hope you haven't found this too slow or too boring. I also hope the reveal style of post has made you think a little and helped you retain what you have learned.

The Best

Below are all the best versions of the functions we came up with so you don't need to look back to find them all.

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

These are the versions you'll likely not need, but could be of interest:

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

You can use these values to test the code:

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:

Comments

Popular Posts