Feature Request: Constructor parameter default value when receiving null input

I see your point and I think such feature could be useful to make the code more concise. It sounds to me like a very Kotlin-ish improvement.

But I’m not sure if it should be really allowed on data classes. Data classes are not just classes with auto-generated methods. They are classes for storing data and therefore they have some specific contract or constraints. These constraints do not make them less useful. Actually, the opposite, they let us make some assumptions on objects behavior that would be impossible otherwise.

You seem to focus on what your proposed change would improve, what use cases would be easier to handle. But you ignore what this change would break, what side effects would it have. This change is not really backwards compatible, it would make some existing code crash. Right now developers could assume that if they create a data class passing some value to it, then it will hold exactly the same value and will return it back. After the change this assumption is no longer true. There could be some libraries that use reflection to analyze data classes, they read parameters of the primary constructor and assume fields and getters have exactly same types. They would also misbehave after the change.

I guess this is the reason why you got negative response from Lombok as well. It would really break constraints of their data classes.

Yes, you are correct, you can use var, add props/functions, etc. to data classes, but it does not really break their contract. Only the “main” props are constrained and should work in a predictable way. You can still add extra props or functions, add some behavior. And yes, we can define a var and modify it in init {}. I think this is a bad idea, but I don’t know why this is not disallowed. Maybe just because it is not worth working on.

2 Likes

Yep, that mapper is awesome, but as I pointed out, it’s only provided by a Kotlin-specific module and requires that the consumer specifically instantiate that mapper, or otherwise configure their mapper to be Kotlin-aware. It’s really not out of the box, so it would still require the consumer to take a reciprocal action for compatibility, meaning that this is by definition a breaking change.

1 Like

Thank you for your response. I appreciate you taking the time to discuss the language reason for declining this request.

I’m not sure I understand how this change would break the existing contract on data classes. The syntax already allows you to specify a default value on omitted parameters, and this proposal doesn’t mean that behavior has to change. It would be an additional, optional behavior that existing code would have to opt-in to in order to use, and the declaration would be clear about the difference.

data class Foo(
  val x: String, // required
  val y: String? = "Bob", // optional, will store null when told to
  val z: String ?: "Joe", // optional, stores "Joe" when param is null
  val q: String ?: throw IllegalArgumentException("More helpful application-specific exception than NPE")
)

Also, I’ve been using the Elvis operator to illustrate this functionality because it seems to be the closest implementation for this, but another operator or annotation would work just as well. I thought kyay10’s @NullDefaults plugin was a great idea, and quite clear that it’s a Java compatibility thing. Having that in the Kotlin standard library would be a great alternative.

1 Like

I think maybe the difference here is that contract-wise you’re changing the constructor to accept null values, even though the data class values do not accept null values. That is technically a slightly different contract, and so some tools that relied on the constructor having exactly the same types as the properties for a data class could now fail.

Fair enough. Is there a concern about that in non-data classes as well? Would this be something that can be allowed in regular classes just to make things more concise?

As for the benefits of data classes, is there a way people can still get a data class’s auto methods while using a non-data? Something similar to Lombok’s annotations that take care of the boilerplate code for you? I think providing a concise way to avoid repetitive manual code would also significantly improve the language (and migrating to it) if such a thing doesn’t already exist.

1 Like

You are now basically asking for the second Lombok implemented for Kotlin :slight_smile:

If you want to avoid boilerplate related to hashCode(), equals() and toString() then you can also help yourself by using Apache Commons library:

class HoledRequest(string: String?, list: List<String>?) {
	val string: String = string ?: ""
	val list: List<String> = list ?: emptyList()
	
	override fun hashCode() = HashCodeBuilder.reflectionHashCode(this)
	
	override fun equals(other: Any?) = EqualsBuilder.reflectionEquals(this, other)
	
	override fun toString() = ToStringBuilder.reflectionToString(this)
}

There is a slight performance penalty here for using reflection, but for those few classes used as request payloads where you can’t declare them as data classes it should be good enough.

1 Like

In other words, no. I was hoping we wouldn’t have to go with third-party solutions for this, but oh well.

Thank you for your time.

1 Like

By changing the contract I actually meant two things. First is a technical/bytecode change: it would mean that the type of the property differs between constructor, field, getter and setter. Right now I believe (?) it is always the same, so tools using reflection, annotation processors, etc. may work improperly.

Second difference is behavioral. Right now you can assume whatever you set to a property of a data class, will be stored there as is. These props are simple data holders, getting or setting them is not associated with any additional behavior or side-effects. In this respect they are similar to local variables. Normally, you assume you have full control over local variables and you don’t expect they store a different value that you just set them to (still, this is possible with property delegates).

Having said that, I’m not a part of the Kotlin team and I’m not authorized to say how data classes should or shouldn’t work. This is just my understanding of these classes.

Regarding non-data classes, I don’t see why not to use such feature. Normal classes don’t have any kind of contract. Unfortunately, I believe it is not possible to automatically generate equals() and other methods for non-data classes.

And I don’t really see how this is a Java interop thing. In both Java and Kotlin we sometimes substitute nulls with default values. In both Java and Kotlin it is generally discouraged to solve the problem of a function with large number of optional params by making them nullable. In both of these languages nulls are considered just one of possible values and not the lack of a value. If you pass a null in varargs, it won’t be skipped, but passed as null. If you set a HashMap item to a null, it won’t become unset, but set to null. new StringBuilder(null) is not considered the same as new StringBuilder() - it throws NPE. And so on. For sure there are some stdlib APIs that treat nulls similar to missing params, but I don’t think this is a common way of doing things in Java/Kotlin.

And I definitely understand about the difference between null and a missing parameter. I agree with that. I’m just saying that there are enough times where null IS treated as equivalent to a missing parameter (by necessity in many forms of serialization) that it would be nice if there were some more common way to handle this case. As a language that actively discourages, and in some cases prohibits, the use of null, Kotlin is ideally suited to give developers more power and convenience in dealing with this.

Perhaps interop was the wrong word to use here since it has more to do with the underlying JVM and not the Java language. But the reality is that people are migrating their code from Java to Kotlin, and many of us can’t do it in one shot, nor can we expect all of our dependencies to keep up. In order to support existing Java code, we’re having to go pretty far out of our way in some cases to do very un-Kotlin things that are going to be harder (mostly to remember) to get rid of once those concerns are no longer an issue.

1 Like

Keep in mind that if Kotlin makes it easier to code up inherited poor designs, it also is making it easier for developers to create those poor designs from scratch. Seeing such an added feature could cause many developers to assume it’s a good design because Kotlin made a special language feature just to support it.

The Java to Kotlin conversion tool should keep your code compatible. You are going out of your way trying to make your code Kotlin-y, not the other way around. Though I wouldn’t call data classes with such init behavior Kotlin-y myself. Kotlin aims to be readable, not concise. Still, Kotlin helps with so many patterns, I understand it’s frustrating when you find a scenario that Kotlin doesn’t make 10 times easier than Java.

1 Like

I think you confuse building the data with using the data. The problem is not how we use a data class but how we build up a data class.

I relay on Kotlin null and field initialisation mechanisms heavily. However, building up 50 field constructors with = "" and = 0 is simply silly and really hurts readability, because the one = 8 is very easy to miss.

I need those 50 fields and I need them on a reasonable default values (not null of course) because they will be used in forms. These forms use Kotlin reflection (not Java as this is browser code) for automatic data binding and I really-really don’t want to go into “what’s not initialised” yet.

To use my data effectively, I had to go around and write a function called default which fills my fields with reasonable default value, so I can do something like this:

val bo = default<MyBo> { field49 = "aaa" }

But this solution is quite slow because of all the reflection it needs. Fine for creating one, problem when creating 10000.

These problems really depend on the application you write. For back-office oriented applications with thousands of fields the default value is an unnecessary hassle. I know what I want for default. Every time.

My problem is very similar to the original question. I actually planned to to write a compiler plugin which creates an additional, empty constructor that calls the default constructor with default values.

Now I plan to convert the one from kyay10 (thank you for that, I need other functions, so I think it’s better to have a separate one, it will be open source as well) into what I need.

1 Like

For what it’s worth, I would also very much like to see @KieferSkunk 's proposed change in Kotlin. This very thing has been a persistent pain for me in Python and it’s unfortunate to see it’s still a persistent pain now that I’ve moved to the (otherwise excellent) Kotlin.

Or an alternative: Allowing us to call functions with syntax func(arg=if (condition) value) (see related thread requesting same feature in Python). This would allow the caller to omit an argument depending on a condition (e.g. if it is null: func(arg=if (value!=null) value)) - triggering replacement by the default.

This would help solve an extremely common problem: Default Duplication. Situation is:

  • Some function f_lowlevel somewhere declares a default value for arg:type=defaultValue.
  • Some function f_highlevel somewhere calls f_lowlevel. We want to have the option to override arg when calling f_highlevel so we pass arg as an argument to f_highlevel. But we still want to maintain the same default. We are left with two choices:
  1. Put arg:type=defaultValue in the signature of f_highlevel (duplicating the default). This is bad because now you have default defined in multiple levels, with an underlying assumption that it should remain the same. Changes to the code can easily break this assumption - and a very common cause of bugs is the assumption that you’re using one default when you’re actually using another.
  2. Make the default a constant DEFAULT_VALUE = 1234, and change the signature of both f_lowlevel and f_highlevel to contain arg:type=DEFAULT_VALUE. This is only possible when you have control of f_lowlevel and is bad because it requires you to anticipate that highlevel code will want to re-use a default when designing lowlevel code.

You may counter-argue that “if you’re doing proper dependency injection then you shouldn’t be passing arguments through multiple levels in the first place”. Well sure - but realistically, code often ends up just not being structured with nice dependency injection, or it would require a big refactor to make it so, and there should be an easy way to fix the problem of duplicated defaults without refactoring.

I would propose using “=?” to indicate “assign to default if null or not provided” (as ?: uses the colon which is already associated with type annotation and ?= is ambiguous with “? =”)

Or - maybe a better alternative - generalize the usage of “?” - so that you can pass an argument as arg = value? - meaning “pass this argument if it is not null”. This seems like a pretty straightforward generalisation of the current usage of “?”

E.g.

    fun printArgValue(a: String = "default") = println(a)
    printArgValue()  // "default"
    printArgValue("hello")  // "hello"
    // Proposed syntax for optional arg-passing
    val arg: String? = null 
    printArgValue(arg?)  // "default"
    val arg: String? = null 
    printArgValue(a=arg?)  // "default"
    val arg: String? = "hello" 
    printArgValue(arg?)  // "hello"

This would also be useful for constructing lists with optional members -

    val a: Int? = 4 
    val b: Int? = null
    val c: Int? = 6
    val myList: List<Int> = listOf(a?, b?, c?)
    println(myList)  // [4, 6]