Looking for a more idiomatic way to do conditional logging during a list map - kotlin

There's gotta be a more Kotlin-esque and terse way to do this in Kotlin, but it's not coming to me. Imagine you're doing a mapNotNull operation. Items which cannot be mapped are converted to null to be filtered out. Items that cannot be mapped also result in a warning being printed. This code works, but it's really verbose. Can you help me trim it down?
val listOfStrings = listOf("1","2","3","not an int", "4","5")
val convertedToInts = listOfStrings.mapNotNull {
val converted = it.toIntOrNull()
if(converted == null){
println("warning, cannot convert '$it' to an int")
}
converted
}

I think your code is idiomatic and readable as it is. I prefer to write it with the explicit null-check.
But if you really want to make a shorter one-liner, you could do something like below. But it looks very hacky with the null.apply {} which is needed to return null instead of Unit from the right side of the elvis-operator:
val listOfStrings = listOf("1","2","3","not an int", "4","5")
val convertedToInts: List<Int> = listOfStrings.mapNotNull {
it.toIntOrNull()
?: null.apply { println("warning, cannot convert '$it' to an int") }
}
You could also use run which looks a bit more readable:
?: run {
println("warning, cannot convert '$it' to an int")
null
}

Related

Kotlin - Type mismatch: inferred type is Any? but Boolean was expected

I'm trying my hands on Kotlin. Being from a Python background is really giving me a tough time to get the knack of the Kotlin syntax. I'm trying to do a simple dictionary (Mutable Map) operation. However, its giving me exceptions.
This is what I tried. Kotlin compiler
Adding the code snippet for reference.
fun main() {
val openActivityMap = mutableMapOf<String, MutableMap<String, Any>>()
val packageName = "amazon"
val currentTime = 23454321234
if(openActivityMap.containsKey(packageName)){
if(openActivityMap[packageName]?.get("isAlreadyApplied")){
if((openActivityMap[packageName]?.get("lastAppliedAt") - currentTime) > 3600){
openActivityMap[packageName]?.put("isAlreadyApplied", false)
}
}
else{
openActivityMap[packageName]?.put("isAlreadyApplied", false)
}
}
}
I'm a bit late to the party, but I'd like to point out another solution here.
As I commented on the OP, heterogeneous maps with fixed string keys like this are usually better expressed with classes in Kotlin. For instance, in your case, the class for your main map's values could be the following:
data class PackageInfo(
var isAlreadyApplied: Boolean,
var lastAppliedAt: Long,
)
(you could obviously add more properties if need be)
This would save you all the casts on the final values.
Another point I'd like to make is that if you access the value for a key anyway, you don't need to check up front the existence of the key with containsKey. Maps return null for keys that are not associated with any value (this is why you need to check for null after getting the value).
The compiler cannot see the correlation between containsKey and the subsequent get or [] access. However, it's smart enough to understand a null check if you simply get the value first and then check for null.
This always applies unless you want to tell the difference between keys that aren't in the map and keys that are in the map but associated null values (which is quite rare).
All in all, I would write something like that:
fun main() {
val openActivityMap = mutableMapOf<String, PackageInfo>()
val packageName = "amazon"
val currentTime = 23454321234
val packageInfo = openActivityMap[packageName]
if (packageInfo != null) { // the key was found and the value is smart cast to non-null in the next block
if (packageInfo.isAlreadyApplied) {
if ((packageInfo.lastAppliedAt - currentTime) > 3600) {
packageInfo.isAlreadyApplied = false
}
} else {
packageInfo.isAlreadyApplied = false
}
}
}
data class PackageInfo(
var isAlreadyApplied: Boolean,
var lastAppliedAt: Long,
)
I would recommend writing tests first and working in small increments, but this should fix your compilation issues:
fun main() {
val openActivityMap = mutableMapOf<String, MutableMap<String, Any>>()
val packageName = "amazon"
val currentTime = 23454321234
if(openActivityMap.containsKey(packageName)){
if(openActivityMap[packageName]?.get("isAlreadyApplied") as Boolean){
if((openActivityMap[packageName]?.get("lastAppliedAt") as Long - currentTime) > 3600){
openActivityMap[packageName]?.put("isAlreadyApplied", false)
}
}
else {
openActivityMap[packageName]?.put("isAlreadyApplied", false)
}
}
}
EDIT: Also I prefer to avoid nullable variables and mutable objects in general, but I suppose there's an exception to every rule.
Couldn't you just declare your Map<String, Any> to return a Boolean instead of Any? So,
val openActivityMap = mutableMapOf<String, MutableMap<String, Boolean>>()
It looks like you're trying to use your second Map to store both Booleans and Ints, which is complicating the logic. You'll need to typecast if you decide to approach it without Typing.
There's a problem with the 2 statement below
if(openActivityMap[packageName]?.get("isAlreadyApplied"))
if((openActivityMap[packageName]?.get("lastAppliedAt") - currentTime) > 3600)
As we all know, an IF statement requires a boolean value for it's param. The types of both statement are unknown at compilation time as they are of a Generic type, Any. As such,
openActivityMap[packageName]?.get("isAlreadyApplied") could be a null or of type Any (Not Boolean).
openActivityMap[packageName]?.get("lastAppliedAt") could be a null or of type Any (an Int was expected here for computation).
This would throw compilation errors as the compiler does not know the types to go with. What could be done is to cast to it's proper types.
Solution
openActivityMap[packageName]?.get("isAlreadyApplied") as Boolean ?: false
((openActivityMap[packageName]?.get("lastAppliedAt") as Int ?: 0) - currentTime)
Giving a default value if it's null.
maybe you can try something like this
if (openActivityMap.containsKey(packageName)) {
val packageMap = openActivityMap[packageName]!!
val applyRequired = (packageMap["lastAppliedAt"] as Long - currentTime) > 3600
packageMap["isAlreadyApplied"] = packageMap.containsKey("isAlreadyApplied") && !applyRequired
}
btw. do you really want to have lastAppliedAt to be in te future? otherewise it will never be > 3600

How to make an extension function that prints all datatypes in kotlin?

I want to make an extension function(named (printDatatype) for example) that can be applied on all datatypes and just prints it ... for example :
"example".printDatatype() ==>output: example
56.prinDatatype()==>output: 56
null.printDatatype()==>output: null
className.printDatatype()==> output: ClassName(property1 = value, property2 = value ....)
Soo something like this?
fun Any?.printLn() = println(this)
Any custom objects will need to override the toString method (like always). That will be auto on data classes.
I honestly don't know what the use case for something like would be.
so i figured it out :)
fun Any?.printMe() {
if (this is Class<*>) {
println(this.toString())
} else if (this.toString() == "null"){
println("null")
}
else {
println(this)
}
}

Kotlin compiler nullability check

I'm a kotlin newby and wondering why the kotlin compiler does not recognize that my variable is never going to be null and how to get around it the nicest way possible.
var myString: String? = null
val myList = mutableListOf<String>()
for (element in 1..2) {
if(myString == null) {
myString = if(*somecondition*) "a" else "b"
}
//compiler error, cannot infer type String? to String
myList.add(myString)
}
A little explaination and how to handle those cases the best way would be nice!
Because String? and String are two different types of variable (or constant). The value of your variable by the time of accessing it might have been changed by another thread/coroutine. In your case i would do something like:
var myString: String? = null
val myList = mutableListOf<String>()
for (element in 1..2) {
myList.add(myString ?: if (/* condition */) "a" else "b")
}
After upgrading to the newest compiler version the code mentioned in the question compiles.

Kotlin nullability check if-else functional approach...How?

For simple check like
if (variable != null) {
doSomething(variable)
}
We could change to
variable?.let { doSometing(it) }
However for a case with else
if (variable != null) {
doSomething(variable)
} else {
doOtherThing()
}
Is there a way of doing so in a single function? Is there something like either?
You can use the elvis-operator ?: like so:
variable?.let { doSomething(it) } ?: doOtherThing()
However personally I think that this gets quite unreadable very quickly. There is nothing wrong with an if-expression.
Another approach which might ring well with functional style is the use of when, which is very close to pattern matching:
when(variable) {
null -> doOtherThingSomething()
else -> doSomething(variable)
}
I would argue, however, that you should be careful with abandoning the classic if statement, if what you're after is its natural purpose - conditional execution. If you're calculating a value in two different ways and then using it in the same way, this approach makes sense. But if your code paths diverge at this point and don't meet again, then if provides a clearer intent.
You can map null-able value if not null by using ternary operator to check not null condition with If...Else Statements.
Here, I had wrote some code snippet to check value null or not ,
Case 1: value initialized
fun main(args: Array<String>) {
val value:Int ?= 10
val mapped = value?.let { "Value is == $value" } ?: "Value not initialized..."
println(mapped)
}
You gotta result: Value is == 10
Case 2: value set remains null
fun main(args: Array<String>) {
val value:Int ?= null
val mapped = value?.let { "Value is == $value" } ?: "Value not initialized..."
println(mapped)
}
You gotta result: Value not initialized...

How can I tell Kotlin that an array or collection cannot contain nulls?

If I create an array, then fill it, Kotlin believes that there may be nulls in the array, and forces me to account for this
val strings = arrayOfNulls<String>(10000)
strings.fill("hello")
val upper = strings.map { it!!.toUpperCase() } // requires it!!
val lower = upper.map { it.toLowerCase() } // doesn't require !!
Creating a filled array doesn't have this problem
val strings = Array(10000, {"string"})
val upper = strings.map { it.toUpperCase() } // doesn't require !!
How can I tell the compiler that the result of strings.fill("hello") is an array of NonNull?
A rule of thumb: if in doubts, specify the types explicitly (there is a special refactoring for that):
val strings1: Array<String?> = arrayOfNulls<String>(10000)
val strings2: Array<String> = Array(10000, {"string"})
So you see that strings1 contains nullable items, while strings2 does not. That and only that determines how to work with these arrays:
// You can simply use nullability in you code:
strings2[0] = strings1[0]?.toUpperCase ?: "KOTLIN"
//Or you can ALWAYS cast the type, if you are confident:
val casted = strings1 as Array<String>
//But to be sure I'd transform the items of the array:
val asserted = strings1.map{it!!}
val defaults = strings1.map{it ?: "DEFAULT"}
Why the filled array works fine
The filled array infers the type of the array during the call from the lambda used as the second argument:
val strings = Array(10000, {"string"})
produces Array<String>
val strings = Array(10000, { it -> if (it % 2 == 0) "string" else null })
produces Array<String?>
Therefore changing the declaration to the left of the = that doesn't match the lambda does not do anything to help. If there is a conflict, there is an error.
How to make the arrayOfNulls work
For the arrayOfNulls problem, they type you specify to the call arrayOfNulls<String> is used in the function signature as generic type T and the function arrayOfNulls returns Array<T?> which means nullable. Nothing in your code changes that type. The fill method only sets values into the existing array.
To convert this nullable-element array to non-nullable-element list, use:
val nullableStrings = arrayOfNulls<String>(10000).apply { fill("hello") }
val strings = nullableStrings.filterNotNull()
val upper = strings.map { it.toUpperCase() } // no !! needed
Which is fine because your map call converts to a list anyway, so why not convert beforehand. Now depending on the size of the array this could be performant or not, the copy might be fast if in CPU cache. If it is large and no performant, you can make this lazy:
val nullableStrings = arrayOfNulls<String>(10000).apply { fill("hello") }
val strings = nullableStrings.asSequence().filterNotNull()
val upper = strings.map { it.toUpperCase() } // no !! needed
Or you can stay with arrays by doing a copy, but really this makes no sense because you undo it with the map:
val nullableStrings = arrayOfNulls<String>(10000).apply { fill("hello") }
val strings: Array<String> = Array(nullableStrings.size, { idx -> nullableStrings[idx]!! })
Arrays really are not that common in Java or Kotlin code (JetBrains studied the statistics) unless the code is doing really low level optimization. It could be better to use lists.
Given that you might end up with lists anyway, maybe start there too and give up the array.
val nullableStrings = listOf("a","b",null,"c",null,"d")
val strings = nullableStrings.filterNotNull()
But, if you can't stop the quest to use arrays, and really must cast one without a copy...
You can always write a function that does two things: First, check that all values are not null, and if so then return the array that is cast as not null. This is a bit hacky, but is safe only because the difference is nullability.
First, create an extension function on Array<T?>:
fun <T: Any> Array<T?>.asNotNull(): Array<T> {
if (this.any { it == null }) {
throw IllegalStateException("Cannot cast an array that contains null")
}
#Suppress("CAST_NEVER_SUCCEEDS")
return this as Array<T>
}
Then use this function new function to do the conversion (element checked as not null cast):
val nullableStrings = arrayOfNulls<String>(10000).apply { fill("hello") }
val strings = nullableStrings.asNotNull() // magic!
val upperStrings = strings.map { it.toUpperCase() } // no error
But I feel dirty even talking about this last option.
There is no way to tell this to the compiler. The type of the variable is determined when it is declared. In this case, the variable is declared as an array that can contain nulls.
The fill() method does not declare a new variable, it only modifies the contents of an existing one, so it cannot cause the variable type to change.