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

Println? Part 1

This post explores various ways to convert and print text Strings to the console. We'll also look into refining the code into shorter, more idiomatic versions.

Rather than focus too much on the technical details, I will aim to focus on practical applications that you can put to use immediately.

Be sure to read to the end to see the final (and hopefully best) forms of the functions in part two (now available)

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.


Situation - Printing Results to the Console

Imagine you need to run a function that returns a String result. We can think about the following:

  1. Does the function just return a String or does it do something else that affects something else in the program?
  2. Do we need to display that result in the console?
  3. Do we need to store that result for use somewhere else?

For these examples, we will focus on numbers one and two being true. 

As a general rule, we want our code to be as easy to read and understand as possible. Keep that in mind for the following.

Example Function

This function may help you understand the type of situation we're dealing with.


private fun testXAndReturnSummary()
: String {
/**
* [processData] alters program
* state and returns enum
* which we convert to String
* using [getTextDescription]
*/
return getTextDescription(
processData()
)
}

Three Standard Println Approaches

There are three main ways we can deal with printing a String returned from a function that is doing something.

  • Approach 1 - Store the result and print it

Perhaps you would get the String then print it.


val resultSummary =
testXAndReturnSummary()

This approach gets a 4/10. It works, but it is quite verbose. We don't need to store the resultSummary so this is definitely not great.

  • Approach 2 - Print the return of the function directly

You might do it like this...

println(testXAndReturnSummary())

It's shorter than the code above, but now the intention is obfuscated. It looks like we are just printing a String value, but we are actually interested in testing something as well. We can score this a 4/10 as well.

  • Approach 3 - Also...

Perhaps you are more experienced with Kotlin and do it like this:


testXAndReturnSummary()
.also { println(it) }

This is great. This is a perfect 10/10; the idiomatic way to print a String being returned from a function. This makes it clear you want the function to do something and you also want to print the result.

Alternative Approach

Approach 3 above is ideal, but we can think about alternatives.

  • Alternative 1

We could try this:

testXAndReturnSummary().println()

But as you may know or notice, this doesn't exist (yet).

How would you add this?

Before clicking for the solution below, think about how you would do this. As a hint, remember the function we are interested in returns a String.
Click to reveal

fun String.println(): String {
println(this)
return this
}

Is this a good solution? It's okay, I guess. I feel it lands somewhere between Approach 2 and 3 so I'll give it a 6.5/10. It's definitely convenient.

  • Alternative 2

We can make a small change to Alternative 1; change the name of the println function. Instead of println, we could name it alsoPrintln(). This small change makes the intent of the code much clearer.

testXAndReturnSummary().alsoPrintln()

I think we could score this solution a 9/10. In personal projects, especially for testing things, I would recommend adding this String extension locally and deleting it later when the project becomes more mature.

Alternative 3

  • Another way to deal with this is to wrap the printing in another function. Now we just simply call the wrapper function:

fun testSomethingAndPrintResult() =
testXAndReturnSummary()
.also { println(it) }

This is an 8.5/10 solution as well in my eyes. It's convenient, the intent is clear, and we can easily write this as a single line of code.

The only issue with this is you would need to do this for every function that returns a String. This does not scale well if you have a lot of functions like these (but it isn't too bad).

What Else?

We can think of other ways to make printing a little more convenient.

Number Formatting


fun Double.alsoPrintln(
decimalPlaces: Int = 2)
: Double {
println("%.${decimalPlaces}f"
.format(this))
return this
}

fun Float.alsoPrintln(
decimalPlaces: Int = 2
): Float {
println("%.${decimalPlaces}f"
.format(this))
return this
}

Adding a simple option to specify the cutoff for decimal places makes working with numbers a lot easier; however, this solution is not ideal.

You'll often need to put those number into a String so you're likely better off adding a toString extension to the number itself:

fun Float.toString(
decimalPlaces: Int = 2
): String {
return "%.${decimalPlaces}f"
.format(this)
}

But thinking about that, it's maybe not the best naming. We can also rewrite this as a single line. Let's make use of these improvements for the Double version of this function. This is a 7/10 solution, I think.

Have a quick think about how you might improve the function name and also think about another way to write this. Don't spend too long on this.
Click to reveal

fun Double.toRoundedString(
decimalPlaces: Int = 2
): String = "%.${decimalPlaces}f"
.format(this)

Okay, so it is one line on my IDE but I had to add line breaks to make it show up nicely on this blog. 

Notice the naming change; there are two very specific reasons for these. I wanted to start the function with 'to' so it shows up in autocomplete along the same pattern as toString

Secondly, toRoundedString makes it obvious you are limiting the decimal places without needing to read the name of the input variable.

I actually think this improved floating-point (real) number extension is very useful so I will rate that a 10/10.

Note: Watch out for Locales! These  functions can be changed/extended to take this into consideration, of course.

Other Values

We can template this, of course. While we're at it, we may as well try to make it a little more convenient to add that value as part of a text String.

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

fun <T> T.alsoPrintln(
start: String = "",
end: String = ""
): T {
println(start + this + end)
return this
}

Okay, so these are not really very useful. 

Most primitive values in Kotlin really need some kind of surrounding text to make them understandable. Just seeing false as a single line in your console is generally not very useful without the surrounding text.

We added start and end Strings to the alsoPrintln function, but at this point it's not really making things nicer or easier to read. 

In fact, I would rate these solutions 2.5/10 (.5 bonus points for being creative with start and end).

In Action

Let's look at some of these in action:


fun templateAndNumberPrintTest() {
13123.alsoPrintln()
false.alsoPrintln()

123.313.alsoPrintln(
decimalPlaces = 3
) // to three places
1233.3434f.alsoPrintln(
decimalPlaces = 1
)
1233.3434f.toRoundedString(
decimalPlaces = 2
)

val widthCm = 1232.213123213
println("The object was ${
widthCm.toRoundedString(
decimalPlaces = 3
)
} wide")

val result = true
result.alsoPrintln(
start = "The result was ",
end = "."
)
println("The result was " +
"${result}.")
}

Looking at this, the primitive values being directly followed by the alsoPrintln is quite amusing, but I doubt it has many real practical applications.

The Double and Float decimalPlaces modifier with alsoPrintln suffers the same fate, but the toString with decimalPlaces looks very practical.

The result value being printed using the start and end version really looks quite bad. The standard println function following that is shorter, more idiomatic, and easier to read.

Call Chaining

As we're looking at examples, we can take a quick look at call chaining:


fun callChainExample() {
val upperName =
"With our extension"
.alsoPrintln()
.uppercase()
.alsoPrintln()
}

Here you can see how convenient the return on the alsoPrintln() is. Of course, we can do this with quite easily with the standard language features:


fun callChainExample2() {
val upperName =
"With standard language features"
.also { println(it) }
.uppercase()
.also { println(it) }
}

Logging

Rather than println, you may want to add a logging extension.

Try to think about how you would write this for yourself before clicking for the solution below. It's not very complicated and follows our earlier patterns. Think carefully about the naming of the function.
Click to reveal

fun String.alsoLogV(
tag: String
): String {
Log.v(tag, this)
return this
}


val stringResult =
"A very interesting result."
stringResult.alsoLogV(
tag = "P_Test"
)

We're following the same also pattern here and allowing the user to specify a tag for the log. This is extremely convenient and possibly a 9/10 as you would need to write a version of this for each type of Log (D, V, E, etc.).

Interesting. Any More?

Yes, but I think this post is getting too long for most readers, especially if you are not familiar with these concepts. 

Experts may have just skimmed through most of this and, hopefully, it just acted as a refresher. They may want to jump straight to part 2.

Less experienced programmers should definitely play with these concepts in their code and think about how they might want to apply some of the lessons to their workflow.

I would definitely recommend reading part 2 before making any of these standard practice as the versions above are not the final recommended forms.

Part 1 Conclusion

We have mainly focused on extension functions, call chaining, and naming, but we also touched upon templates. In part two, we will go into a little more depth.

I'll leave you with one final note - these are not the final versions of these functions. The final versions will be presented in part two.

Links

  • Part 2

  • Raw Kotlin

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