Я пробую идиоматический и идеально функциональный способ разбить список на подсписки в Kotlin.
Представьте, что вводом является ["aaa", "bbb", "", "ccc", "", "ddd", "eee", "fff"]
, я хочу вернуть [["aaa", "bbb"], ["ccc"], ["ddd", "eee", "fff"]]
для данного предиката string.isEmpty()
.
Это довольно просто сделать с помощью цикла for и аккумулятора; но я не нашел способа написать его функционально, который я считаю достаточно читаемым.
На данный момент мой лучший результат:
lines.foldIndexed(Pair(listOf<List<String>>(), listOf<String>()), { idx, acc, line ->
when {
idx + 1 == lines.size -> {
Pair(acc.first + listOf(acc.second + line), listOf())
}
line.isEmpty() -> {
Pair(acc.first + listOf(acc.second), listOf())
}
else -> {
Pair(acc.first, acc.second + line)
}
}
}).first
По сути, я использую fold
с двойным аккумулятором, который отслеживает текущий список и сбрасывает его, когда найден предикат. Список подается в полный результат в этой точке. Я использую foldIndexed, чтобы получить свой последний список.
Вы, ребята, знаете какой-нибудь лучший способ?
Для справки, версия цикла может быть
val data = mutableListOf<String>()
var currentData = ""
for(line in lines){
if (line.isEmpty()) {
data.add(currentData)
currentData = ""
}
else{
currentData = "$currentData $line"
}
}
data.add(currentData)
Спасибо !
предполагая, что вы бы связали его, если бы он у вас был, но у вас случайно нет ссылки на вопрос? Спасибо!
Нашел: stackoverflow.com/q/65140871/506796
Обратите внимание, как CharSequence.split
стандартной библиотеки похож на ваш декларативный код: github.com/JetBrains/kotlin/blob/…. Функции функционального стиля — это просто маски над декларативным кодом, поэтому я думаю, что если вы используете что-то неоднократно, вам нужно создать свои собственные функции функционального стиля для этих случаев.
Потрясающе, спасибо! И правда хорошие предложения. Я стараюсь практиковать FP как можно больше, но я также думаю, что это должно иметь смысл. В этом случае я еще не совсем уверен и надеялся на что-то, что я бы пропустил :)
Я бы предложил сначала найти точки разделения (вручную добавив индексы ребер), а затем сделать срезы:
val lines = listOf("aaa", "bbb", "", "ccc", "", "ddd", "eee", "fff")
val result = lines
.flatMapIndexed { index, x ->
when {
index == 0 || index == lines.lastIndex -> listOf(index)
x.isEmpty() -> listOf(index - 1, index + 1)
else -> emptyList()
}
}
.windowed(size = 2, step = 2) { (from, to) -> lines.slice(from..to) }
println(result) //[[aaa, bbb], [ccc], [ddd, eee, fff]]
Спасибо! Идея мне действительно нравится, это выглядит немного лучше, чем таскать с собой пару аккумуляторов.
Как бы я включил точки разделения в качестве первого элемента в подсписке? В моем случае я хочу разделить по заголовкам и включить каждый элемент до следующего заголовка.
@Sam, я считаю, что вам нужно настроить эту строку: «x.isEmpty () -> listOf (index - 1, index + 1)». Если вы хотите включить элементы точки разделения в результат в качестве первого элемента, он должен стать " x.isEmpty() -> listOf (индекс - 1, индекс)". Если ваша точка разделения не является пустой строкой, вам также необходимо изменить условие «x.isEmpty()» в зависимости от того, какой «заголовок» в вашем случае.
Вот рекурсивная версия.
fun <T> List<T>.split(predicate: (T) -> Boolean): List<List<T>> {
val idx = this.indexOfFirst(predicate)
return if (idx == -1) {
listOf(this)
} else {
return listOf(this.take(idx)) + this.drop(idx + 1).split(predicate)
}
}
fun main() {
val lines = listOf("aaa", "bbb", "", "ccc", "", "ddd", "eee", "fff")
println(lines.split { it.isEmpty() }) // [[aaa, bbb], [ccc], [ddd, eee, fff]]
}
Я не проводил никаких тестов, но предполагаю, что производительность не велика, потому что он дважды выполняет итерацию по списку, один раз для поиска совпадений предикатов и один раз для разделения списка. Можно использовать slice
или subList
вместо take
, а drop
дает лучшую производительность.
У кого-то был точно такой же вопрос на прошлой неделе. Я не думаю, что был найден особенно чистый способ сделать это. Ваш декларативный цикл чище всего, что я видел.