1

I have problem with Kotlin nullability and I'm wondering am I able to resolve it with contracts. For such Java interface: interface Action<T>{ void execute(T param); } there is two extensions:

fun <T, R> Action<T>.map(mapper:(R)->T): Action<R> {
   return Action{ execute(mapper(it)) }
}

and

fun <T> Action<T>.ifNotNull(): Action<T> {
  return Action { if(it != null) execute(it) }
} 

There is also a generic model with nullable data:

class Model<T>(val data: T?)

Now I have created function which take Action interface as argument. Case is to execute action argument only when param != null, so it looks like below:

fun <T> callback(model: Model<T>, action: Action<T>){
    action
    .map{ it.getData() } //compilation error: getData return T? when action require T
    .ifNotNull() //execute only when data!=null
    .execute(model)
}

So now, is there any option to use Kotlin contract to ensure compiler that action will not execute with null parameter?

Willi Mentzel
  • 27,862
  • 20
  • 113
  • 121
Karol Kulbaka
  • 1,136
  • 11
  • 21

3 Answers3

1

ModelAction in your own answer simply provides the correct signature for ifNotNull():

fun <T> Action<T>.ifNotNull(): Action<T?> {
    return Action { if(it != null) execute(it) }
} 

Then you've got the order of operations wrong:

fun <T> callback(model: Model<T>, action: Action<T>){
    action
    .ifNotNull() // Action<T?>
    .map { model: Model<T> -> model.data } // Action<Model<T>>
    .execute(model)
}

Note that the compiler won't be able to infer R for this map usage. You could also write it as

fun <T> modelAction(action: Action<T>): Action<Model<T>> {
    return action
    .ifNotNull()
    .map { it.data }
}

As a side note, the argument is the "wrong way around" for map; such functions are more commonly called contramap.

Alexey Romanov
  • 167,066
  • 35
  • 309
  • 487
0

All type parameters are shipped as nullable by default if unbounded (in other words, they are derived from Any?). Easy way to fix this is just to specify non-null bound on type parameter: <T : Any>

nyarian
  • 4,085
  • 1
  • 19
  • 51
  • You mean interface methd parameter? – Karol Kulbaka Nov 28 '18 at 16:40
  • `fun print(obj: T) { println(obj!!) }` - here `T` is called type parameter, because for this function it is a parameter that can be specified on invocation and only accept some **type**. "The type parameter section, delimited by angle brackets (<>), follows the class name. It specifies the type parameters (also called type variables) T1, T2, ..., and Tn.": https://docs.oracle.com/javase/tutorial/java/generics/types.html – nyarian Nov 28 '18 at 16:44
  • I always try to avoid `!!`, it will throw NPE sooner or later. Beyond this it won't work. It's not possible to store null in nonnull reference, that's why I'm asking about `contract` – Karol Kulbaka Nov 28 '18 at 17:50
  • `fun callback(model: Model, action: Action)` won't work? – nyarian Nov 28 '18 at 17:53
  • That's because `Model` is specified to receive nullable parameter at it's declaration site, though nullability specification can be shipped via type parameter itself (`callback(...)`). It will work if you'll remove a question mark near to type of `data`. – nyarian Nov 28 '18 at 17:56
  • And that's why compilation error is actually a contract that `action` won't be executed with nullable parameter. If you want to retain nullability of `T` in `Model`, then null check ( https://kotlinlang.org/docs/reference/null-safety.html ) is the only option. – nyarian Nov 28 '18 at 18:02
  • `T : Any` won't even compile :). `Model` will never be null. It's single read object. Data is nullified after first read. `Let` won't help also. There is one nullcheck already :> – Karol Kulbaka Nov 28 '18 at 18:09
  • Almost all of these examples do not compile (for instance, because type params declaration comes after `fun` keyword, which is in ` fun Action.map(mapper:T->R)`). If you could provide valid definitions of all the elements then I could reproduce the error and probably create a null-safe implementation... – nyarian Nov 28 '18 at 18:13
  • I've made fixes. Sorry for bugs. – Karol Kulbaka Nov 28 '18 at 18:23
  • If you also could provide the complete definition of `Action` - it would be just perfect! – nyarian Nov 28 '18 at 18:25
  • Missing keywords are added, it's complete :> – Karol Kulbaka Nov 28 '18 at 18:27
  • Action interface has to be in java. I mentioned about this. – Karol Kulbaka Nov 28 '18 at 20:26
  • `Model` returns just some object typed with `T`, whereas `map` function expects a **function type** which receives object of some type `T1` and returns object of some type `T2`. Worth mentioning, also, that `Action` **does not produce anything**, then `map` function is pointless for it. I get the next error: "Type inference failed: Not enough information to infer parameter R in `fun Action.map(mapper: (R) → T): Action`Please specify it explicitly.", which means that here is type issue, not nullability... – nyarian Nov 28 '18 at 21:01
  • Why `fun callback(model: Model, action: Action) { action.execute(model.data) }` and `class Model(val data: T)` does not fit you? Here nullability is excluded by upper bound of `Any` instead of default `Any?` – nyarian Nov 28 '18 at 21:01
  • As I said `data` is set to null after first read. It is requirement. Action doesn't have to produce anything. `map` function just allow to execute action with diferent parameter than expected, but it still came under kotlin generic nulability. I'm looking a clear and safe way to warkaround this problem, and that is the case. – Karol Kulbaka Nov 29 '18 at 05:08
  • "Contract" for this situation is non-nullable type, and if you explicitly specify that argument is nullable, how it is possible to omit null checks, then? It is, also, more complicated because it is Java interface and `platform type` comes into play: https://kotlinlang.org/docs/reference/java-interop.html#null-safety-and-platform-types . – nyarian Nov 29 '18 at 08:03
  • The only thing that can be done here is just making code more declarative and concise using Kotlin features, such as `fun callback(model: Model, action: Action) { model.data?.let(action::execute) }`. Here action won't be executed unless data is not null. – nyarian Nov 29 '18 at 08:03
  • It would work. But code I provided is only sample which show how mechanism works. In app `execute` function is called at low level where I can't make such nullcheck. Gues it can't be done as I would like. @Andrey Ilyunin Thanks for help :) – Karol Kulbaka Nov 29 '18 at 09:39
  • Did all I could... I hope that may be someone smarter will get here and will be able to help you:) – nyarian Nov 29 '18 at 09:44
0

I have created Action interface implementation dedicated for this usage as below:

class ModelAction<T>(val action: Action<T>) : Action<T?> {
    override fun execute(param: T?) {
        param?.let {
            action.execute(it)
        }
    }
}

fun callback<T>(model: Model<T>, action: Action<T>){
    ModelAction(action)
    .map{ it.getData() } 
    .execute(model)
}

This idea may help somebody with similar problem, but it isn't fit my expectations and I still count on solution based on higher-order function.

Karol Kulbaka
  • 1,136
  • 11
  • 21