What is a the idiomatic way to use either in a fun...
# arrow
k
What is a the idiomatic way to use either in a function that deals with nullables. I tried making an example. The point is that the error correctly shows where the function failed. This works, but it's kinda hard to read?
Copy code
fun foo(): String? = if ((0..1).random() == 1) "Hello World" else null
    fun bar(s: String): String? = if ((0..1).random() == 1) s else null

    fun maybe(foo: () -> String?, bar: (input: String) -> String?): Either<Err, String> =
        foo()?.let { first ->
            bar(first)?.let { second ->
                Either.Right(second)
            } ?: Either.Left(Err("Could not find second"))
        } ?: Either.Left(Err("Could not find first"))


    maybe(::foo, ::bar)
Here is a real world example of the same thing. I am working with some unreliable data.
Copy code
private fun CollectionMetadata.getMrsJdbcTypeForColumn(
    tableName: String,
    columnName: String): Either<MrsJsonReaderError, JDBCType> = tables.firstOrNull {
        it.name == tableName
    }?.let { table -> table.columns.firstOrNull {
        it.name == columnName
    }?.let { column -> Either.Right(column.jdbcType) }
        ?: Either.Left(MrsJsonReaderError.MrsColumnNameNotInMetadataForTable(tableName, columnName))
} ?: Either.Left(MrsJsonReaderError.MrsTableNameNotInMetadata(tableName))
c
With the either block you could have something like (if i dont´t wrong):
Copy code
private suspend fun CollectionMetadata.getMrsJdbcTypeForColumn(
    tableName: String,
    columnName: String
): Either<MrsJsonReaderError, JDBCType> =
    either {
        val table =
            tables.firstOrNull()
            ?: shift(MrsJsonReaderError.MrsTableNameNotInMetadata(tableName))
        val column =
            table.columns.firstOrNull { it.name == columnName }
            ?: shift(MrsJsonReaderError.MrsColumnNameNotInMetadataForTable(tableName, columnName))
        column.jdbcType
    }
another option i would try:
Copy code
private fun CollectionMetadata.getMrsJdbcTypeForColumn(
    tableName: String,
    columnName: String
): Either<MrsJsonReaderError, JDBCType> =
    tables.firstOr(MrsJsonReaderError.MrsTableNameNotInMetadata(tableName)) {
        it.name == tableName
    }.flatMap { table ->
        table.columns.firstOr(MrsJsonReaderError.MrsColumnNameNotInMetadataForTable(tableName, columnName)) {
            it.name == columnName
        }.map { column ->
            column.jdbcType
        }
    }

public inline fun <E, T> Iterable<T>.firstOr(error: E, predicate: (T) -> Boolean): Either<E, T> {
    for (element in this) if (predicate(element)) return element.right()
    return error.left()
}
k
That does look like a very nice solution, thank you :)
s
you can use the
either
computation block +
ensureNotNull
Copy code
either<String, String> {
        val first = ensureNotNull(foo()) { "First is null" }
        val second = ensureNotNull(bar(first)) { "Second is null" }
        second
    }
I used a plain string instead of Err
p
It seems @stojan got there first, but the full equivalent example would be:
Copy code
private suspend fun CollectionMetadata.getMrsJdbcTypeForColumn(
    tableName: String,
    columnName: String
): Either<MrsJsonReaderError, JDBCType> = either {
    val table = ensureNotNull(tables.firstOrNull { it.name == tableName }) {
        MrsJsonReaderError.MrsTableNameNotInMetadata(tableName)
    }
    val column = ensureNotNull(table.columns.firstOrNull { it.name == columnName }) {
        MrsJsonReaderError.MrsColumnNameNotInMetadataForTable(tableName, columnName)
    }
    column.jdbcType
}
Also of note, the
firstOr
extension proposed would create the error instance even if it’s not used. That may or may not be an issue for you and is unlikely to have much of a performance impact but worth pointing out (whereas the
ensureNotNull
lazily instantiates it only when required)
Also, you can use the fact
ensureNotNull
defines a contract for smart-casting and unwrap it’s usage:
Copy code
private suspend fun CollectionMetadata.getMrsJdbcTypeForColumn(
    tableName: String,
    columnName: String
): Either<MrsJsonReaderError, JDBCType> = either {
    val table = tables.find { it.name == tableName }
    ensureNotNull(table) { MrsJsonReaderError.MrsTableNameNotInMetadata(tableName)}
    val column = table.columns.find { it.name == columnName }
    ensureNotNull(column) { MrsJsonReaderError.MrsColumnNameNotInMetadataForTable(tableName, columnName) }
    column.jdbcType
}
This (and using
find
in place of
firstOrNull
) keeps the intent of the code nice and clear at each stage.
k
Thanks for the detailed response, that does look very nice as well.