High order function as parameter


#1

I would like to add an extension function java.sql.ResultSet. The Java class has its getters for each type of data (getXXX), but it handles null values akwardly: one must call the wasNull function afterward.

I managed to make an extension function like this:

inline fun <T> ResultSet.nullable(columnIndex: Int, nullValue : T? = null, getter: (Int) -> T) : T? {
    val res = getter(columnIndex)
    return if (this.wasNull()) null else res
}

but the calling of it is a little inconvenient:

rs.nullable("id") { rs.getLong(it) }
rs.nullable("name") { rs.getString(it) }

What I would like to achieve is a calling syntax of

rs.nullable("id", ResultSet::getLong)

Could the extension function be altered to accept function reference with the above syntax?


#2

If I’m not mistaken, you can already do the following:

rs.nullable("id", rs::getLong)

#3

You are right. I made the mistake to use ResultSet::getLong instead of rs::getLong.

However, I ran into a problem. There are two getLong (getLong(int), getLong(String)) functions in ResultSet, so I made two nullable extension functions:

inline fun <T> ResultSet.nullable(columnIndex: Int, nullValue : T? = null, getter: (Int) -> T) : T? {
	val res = getter(columnIndex)
	return if (this.wasNull()) null else res
}


inline fun <T> ResultSet.nullable(column: String, nullValue : T? = null, getter: (String) -> T) : T? {
	val res = getter(column)
	return if (this.wasNull()) null else res
}

Strangely, when using rs.nullable("id", rs::getLong) the compiler complains overload resolution ambiguity. I thought, because the first argument is a String, only the second function applicable, so only rhe getLong(String) is usable. However, now I learnt that the pick of function is done before the higher order function is looked up.


#4

I haven’t tested this, so I can’t verify that it works, but the following might be fine (and DRY up your code)

inline fun <T, Q> ResultSet.nullable(column: Q, nullValue : T? = null, getter: (Q) -> T) : T? {
	val res = getter(column)
	return if (this.wasNull()) null else res
}

that way, whatever you pass into column will be what the function expects (and the compiler should choose the right one).


#5

I’ve already tried that, and didn’t work. But finally I found the problem and it makes everything clear (as soon as one finds it ;-))

I tried the following: rs.nullable("id", null, rs::getLong) and it worked.
Then I tried: rs.nullable("id", getter = rs::getLong) and it worked too :slight_smile:

When I called it without the null value specified, rs::getLong stood on the second unnamed position. The error reported was not because the compiler couldn’t pick one of the getLong functions, but because neither of them was right for the second argument.

So, I had four options:

  • Let it as it is now and the user will be forced to either specify null value, use the lambda syntax or use named parameter
  • Switch the position of the two arguments, but it makes lambda syntax impossible
  • Make two overloaded versions: one without null value and one with required null value
  • Skip null value and let it handled by elvis operator

The first two is inconvenient, so dropped, but the second two options are valid. And although making the overloaded version is straightforward:

inline fun <T> ResultSet.nullable(column: String, getter: (String) -> T) : T? 
		= nullable(column, null, getter)

inline fun <T> ResultSet.nullable(column: String, nullValue : T?, getter: (String) -> T) : T? {
	val res = getter(column)
	return if (this.wasNull()) nullValue else res
}

I think it doesn’t prove its worth:

rs.nullable("id", 42, rs::getLong)
rs.nullable("id", rs::getLong) ?: 42

The second is more Kotlin-like and has one more advantage: it has the type of T, while the nullValue version has the type of T? and would need !!.