Idiomatic Kotlin Value Mapping

I have an extension function for an interface that will replace a single argument in the path and return a new anonymous implementation of that interface. When applying this pattern for multiple arguments, though, I’ve only been able to get it to work by iterating over the map of arguments and values and assigning the result to a var.

The reassignment of a var in this implementation doesn’t feel like it’s the “right” approach, though. Is there a more idiomatic way to accomplish the same goal?

interface NavRoute {
    val path: String
    val title: Int
}

fun NavRoute.withArgument(argument: String, value: Any) : NavRoute {
    val newPath = this.path.replace("{$argument}", value.toString())
    val thisTitle = this.title

    return object : NavRoute {
        override val path: String = newPath
        override val title: Int = thisTitle
    }
}

fun NavRoute.withArguments(arguments: Map<String, Any>) : NavRoute {
    var newRoute = this
    arguments.map {
        newRoute = newRoute.withArgument(it.key, it.value)
    }
    return newRoute
}

I attempted to rewrite this using a recursive approach, but I couldn’t ever get anything to compile.

  • “I attempted to rewrite this using a recursive approach”. Then I suggest you include that attempt, and explain why you are having issues getting it to compile. That’s a much better question that we can actually answer.

    – 

You can avoid var like so:

fun NavRoute.withArguments(arguments: Map<String, Any>): NavRoute =
    arguments.toList().fold(this) { route, pair -> route.withArgument(pair.first, pair.second) }

Your version is more readable though.

BTW, you have "{$argument}", is that really what you want?

What role does the extension have that is different from the interface having a default method that implements the logic?

interface NavRoute {
    val path: String
    val title: Int

    fun copyWithArgs(args: Map<String, Any>): NavRoute {
        var pathWithArgs = path
        args.forEach { arg -> pathWithArgs = pathWithArgs.replace("{${arg.key}}", arg.value.toString()) }
        return object : NavRoute {
            override val path: String = pathWithArgs
            override val title: Int = this@NavRoute.title
        }
    }
}

fun NavRoute.withArgument(argument: String, value: Any): NavRoute = copyWithArgs(mapOf(argument to value))

fun NavRoute.withArguments(arguments: Map<String, Any>): NavRoute = copyWithArgs(arguments)

Assuming this is your real case and not a simplified example for the StackOverflow, we don’t really need to create a separate instance of NavRoute for each argument in the map. Apparently, we only modify the path, so we can do exactly this – modify the path alone:

fun NavRoute.withArguments(arguments: Map<String, Any>) = object : NavRoute {
    override val path: String = arguments.toList()
        .fold(this@withArguments.path) { path, (argument, value) ->
            path.replace("{$argument}", value.toString())
        }

    override val title: Int = this@withArguments.title
}

Also, I suggest an alternative approach. As the pattern for argument placeholders in the path is clearly defined, we can use regular expressions to find these placeholders and replace them with values. This way we don’t have to invoke replace() multiple times and create N strings in the process, but instead replace all arguments in a single pass. Such approach could be potentially better for the performance:

private val regex = Regex("""\{(\w+)}""")
fun NavRoute.withArguments(arguments: Map<String, Any>) = object : NavRoute {
    override val path: String = this@withArguments.path
        .replace(regex) { m ->
            arguments.getOrDefault(m.groupValues[1], m.groupValues[1]).toString()
        }

    override val title: Int = this@withArguments.title
}

Leave a Comment