Necesito abordar una API que devuelve datos fragmentados.
val response = server.getChunk(request, chunkNumber) data class PageInfo( val pageNumber: Int, val maxPages: Int ) data class Response( val elements: List<Payload>, val pageInfo: PageInfo )Supongo que esta API es idempotente.
Puedo hacer algo así ( toma 1 ):
var i = 0 var maxPages: Int val payloadTotal = mutableListOf<Payload>() do { val response = server.getChunk(request, i++) maxPages = response.pageInfo.maxPages payloadTotal.addAll(response.payload) } while (i < maxPages)Funciona, pero es feo.
tomar 2 :
val partners = IntStream .iterate(0) { it + 1 } .asSequence() .map { server.getChunk(request, it) } .takeWhile { it.pageInfo.pageNumber < it.pageInfo.maxPages } .map { it.payload } .flatten() .toList() Se ve mucho mejor, pero no funciona del todo como se esperaba: dado que verificamos el predicado takeWhile después de solicitar el servidor, siempre terminaremos haciendo una solicitud adicional. Y dado que no conocemos el valor de maxPages antes de hacer la primera solicitud, no podemos usar algo como take(n: Int) .
Así que terminé yendo con ese código ( toma 3 ):
var maxPages = Int.MAX_VALUE val partners = IntStream .iterate(0) { it + 1 } .asSequence() .takeWhile { it < maxPages } .map { server.getChunk(request, it) .also { response -> maxPages = response.pageInfo.maxPages } } .map { it.payload } .flatten() .toList()Funciona. Pero, de nuevo, usamos este estado adicional que no me gusta.
Además, debido a las restricciones "efectivamente definitivas", esto no funcionará en Java en absoluto (y me gustaría que lo hiciera como bonificación).
Entonces, ¿cómo logro el comportamiento requerido con un enfoque funcional? ¿Hay alguna manera de hacerlo sin rastrear ningún estado adicional fuera de la canalización?
*Si tal forma existe, ¿también se traduce a Java?
Podemos crear nuestro propio operador de secuencia para este propósito. Requiere escribir algo de código, pero luego podemos usarlo muy bien:
generateSequence(0) { it + 1 } .map { server.getChunk(request, it) } .takeNextWhile { it.pageInfo.pageNumber + 1 < it.pageInfo.maxPages } .flatMap { it.elements } .toList() fun <T> Sequence<T>.takeNextWhile(predicate: (T) -> Boolean): Sequence<T> = Sequence { val iter = iterator() var hasNext = iter.hasNext() object : Iterator<T> { override fun hasNext() = hasNext override fun next() = iter.next().also { hasNext = iter.hasNext() && predicate(it) } } } Alternativamente, podemos takeWhilePrevious() si eso tiene más sentido para nosotros.
Si no queremos crear nuevos operadores, creo que es mejor no usar transformaciones funcionales, ya que empeoran la legibilidad del código en este caso. Suponiendo que maxPages no cambie, primero podemos obtenerlo y luego realizar un ciclo muy simple:
val first = server.getChunk(request, 0) val payloads = first.elements.toMutableList() (1 until first.pageInfo.maxPages).forEach { payloads += server.getChunk(request, it).elements }Pequeña solución para que funcione, puede pasar el objeto de respuesta nula como semilla para generateSequence
val partners = generateSequence(0 to (null as Response?)) { (page, _) -> page+1 to getChunk(page) }.takeWhile { (page, response) -> response == null || page < response.maxPages }.flatMap { it.second?.payload ?: emptyList() }.toList()(Sin embargo, el objetivo de legibilidad aún no está resuelto). Se puede introducir cierta claridad usando funciones
data class PageResponse(val page: Int = 0, val response: Response? = null) { fun isNotLastPage() = response == null || page < response.maxPages fun getPayload() = response?.payload ?: emptyList() } val partners = generateSequence(PageResponse()) { (page, _) -> PageResponse(page + 1, getChunk(page)) } .takeWhile(PageResponse::isNotLastPage) .flatMap(PageResponse::getPayload) .toList()En este caso, probablemente haría una secuencia a partir de un bucle do-while while. Es bastante legible para aquellos que necesitan leer la implementación y bastante útil para aquellos que solo se preocupan por la secuencia:
fun Server.requestInPages(request: Request, startPage: Int = 0): Sequence<Response> = sequence { var page = startPage do { val response = getChunk(request, page++) yield(response) } while (response.pageInfo.pageNumber < response.pageInfo.maxPages) }Y luego utilízalo así:
val partners = server.requestInPages(request) .flatMap { it.elements } .toList() Si las respuestas son todas iguales, incluso podría agregar otro asistente que contenga flatMap encima de requestInPages . Si a nadie le importa la estructura de respuesta y PageInfo en absoluto, incluso podría hacer que flatMap parte de requestInPages .
Puntos de bonificación: esto incluso podría convertirse en un flujo si su llamada API se convierte en una función de suspend en algún momento (por ejemplo, si cambia a rutinas).