2
\$\begingroup\$

I want to split a list (as if splitting a string by a delimiter), ideally in a functional style, and was surprised how hard it seems to be. Or maybe my code is just unnecessarily complicated?

fun <ElemType : Comparable<ElemType>> Iterable<ElemType>.split(delim: ElemType): 
List<List<ElemType>> {
    return this.fold(emptyList<List<ElemType>>()) { 
        acc: List<List<ElemType>>, elem: ElemType ->
        if (elem == delim) 
            acc + listOf(emptyList<ElemType>())
        else if (acc.isEmpty()) 
            listOf<List<ElemType>>(listOf<ElemType>(elem)) 
        else 
            acc.take(acc.size - 1) + listOf(acc.last() + elem)
    }
}

fun main() {
    val l = listOf("1", "", "2", "3", "", "", "4")
    println(l.split(""))
    
    val l2 = listOf(1, 0, 2, 3, 0, 0, 4)
    println(l2.split(0))
}

The result for both cases is [[1], [2, 3], [], [4]].

Kotlin Playground

\$\endgroup\$
3
  • \$\begingroup\$ Out of curiosity, why "ideally in a functionally style"? \$\endgroup\$ Dec 12, 2022 at 16:18
  • \$\begingroup\$ Because I can write procedural code in a lot of languages, but am learning functional idioms in Kotlin. \$\endgroup\$ Dec 12, 2022 at 16:22
  • \$\begingroup\$ Well you chose a tough assignment to learn functional idioms with xD But possibly if you would have handled it list-by-list by searching for the delimiters instead of element-by-element it might have been easier. \$\endgroup\$ Dec 12, 2022 at 18:57

1 Answer 1

2
\$\begingroup\$

It's complicated because you are making it unnecessarily complicated.

I wouldn't use .fold to accomplish this.

Think about how many list operations this line does: acc.take(acc.size - 1) + listOf(acc.last() + elem)

  • acc.take(acc.size - 1) copies the current list of sublists except the last.
  • listOf(acc.last() + elem) (I believe this could be just acc.last() + elem?) creates a new list with one element appended to the last list.

We know that once we've seen a delimiter, we don't need to touch any previous lists, we can just continue looking at what's ahead of us.

This makes it a perfect choice for a sequence builder.

fun <ElemType> Iterable<ElemType>.split(delim: ElemType): List<List<ElemType>> {
    return sequence {
        val currentList = mutableListOf<ElemType>()
        for (elem in this@split) {
            if (elem == delim) {
                yield(currentList.toList())
                currentList.clear()
            } else {
                currentList.add(elem)
            }
        }
        yield(currentList.toList())
    }.toList()
}

Other notes: Your function requires ElemType to extend Comparable<ElemType>. I do not see any reason for this. You can remove that restriction.

\$\endgroup\$
3
  • \$\begingroup\$ Thanks, I thought I needed Comparable for elem == delim, but I now realize that equals is defined for all objects. \$\endgroup\$ Dec 12, 2022 at 16:14
  • \$\begingroup\$ And maybe I don't even need the toList() at the end ... this could even be lazy. \$\endgroup\$ Dec 12, 2022 at 16:20
  • 1
    \$\begingroup\$ @FelixDombek Yup, if possible I would recommend using it as a Sequence instead of a List \$\endgroup\$ Dec 12, 2022 at 17:48

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.