diff --git a/arrow-libs/fx/arrow-fx-coroutines/api/arrow-fx-coroutines.api b/arrow-libs/fx/arrow-fx-coroutines/api/arrow-fx-coroutines.api index ceec1f97474..9400aa58ce8 100644 --- a/arrow-libs/fx/arrow-fx-coroutines/api/arrow-fx-coroutines.api +++ b/arrow-libs/fx/arrow-fx-coroutines/api/arrow-fx-coroutines.api @@ -373,6 +373,7 @@ public final class arrow/fx/coroutines/ResourceKt { public static final fun releaseCase-zgiIeyo (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;)Larrow/fx/coroutines/Resource; public static final fun resource (Lkotlin/jvm/functions/Function1;)Lkotlin/jvm/functions/Function1; public static final fun sequence (Ljava/lang/Iterable;)Larrow/fx/coroutines/Resource; + public static final fun traverse (Ljava/lang/Iterable;Lkotlin/jvm/functions/Function1;)Larrow/fx/coroutines/Resource; public static final fun traverseResource (Ljava/lang/Iterable;Lkotlin/jvm/functions/Function1;)Larrow/fx/coroutines/Resource; } diff --git a/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Resource.kt b/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Resource.kt index 5140cd1d784..8fef5fbe0b6 100644 --- a/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Resource.kt +++ b/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Resource.kt @@ -2,13 +2,8 @@ package arrow.fx.coroutines import arrow.core.continuations.AtomicRef import arrow.core.continuations.update -import arrow.core.NonEmptyList -import arrow.core.ValidatedNel import arrow.core.identity -import arrow.core.invalidNel import arrow.core.prependTo -import arrow.core.traverse -import arrow.core.valid import arrow.fx.coroutines.continuations.ResourceScope import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CancellationException @@ -17,6 +12,7 @@ import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.withContext +import kotlin.experimental.ExperimentalTypeInference /** * [Resource] models resource allocation and releasing. It is especially useful when multiple resources that depend on each other @@ -48,7 +44,6 @@ import kotlinx.coroutines.withContext * suspend fun processData(): List = throw RuntimeException("I'm going to leak resources by not closing them") * } * - * //sampleStart * suspend fun main(): Unit { * val userProcessor = UserProcessor().also { it.start() } * val dataSource = DataSource().also { it.connect() } @@ -59,11 +54,10 @@ import kotlinx.coroutines.withContext * dataSource.close() * userProcessor.shutdown() * } - * //sampleEnd * ``` * * In the following example, we are creating and using a service that has a dependency on two resources: A database and a processor. All resources need to be closed in the correct order at the end. - * However this program is not safe because it is prone to leaking `dataSource` and `userProcessor` when an exception or cancellation signal occurs whilst using the service. + * However, this program is not safe because it is prone to leak `dataSource` and `userProcessor` when an exception or cancellation signal occurs whilst using the service. * As a consequence of the resource leak, this program does not guarantee the correct release of resources if something fails while acquiring or using the resource. Additionally manually keeping track of acquisition effects is an unnecessary overhead. * * We can split the above program into 3 different steps: @@ -99,8 +93,11 @@ import kotlinx.coroutines.withContext * * # Using and composing Resource * + * Arrow offers the same elegant `bind` DSL for Resource as you might be familiar with from Arrow Core. + * * ```kotlin * import arrow.fx.coroutines.* + * import arrow.fx.coroutines.continuations.resource * * class UserProcessor { * fun start(): Unit = println("Creating UserProcessor") @@ -119,7 +116,6 @@ import kotlinx.coroutines.withContext * suspend fun processData(): List = userProcessor.process(db) * } * - * //sampleStart * val userProcessor = resource { * UserProcessor().also(UserProcessor::start) * } release UserProcessor::shutdown @@ -129,11 +125,12 @@ import kotlinx.coroutines.withContext * } release DataSource::close * * suspend fun main(): Unit { - * userProcessor.parZip(dataSource) { userProcessor, ds -> + * resource { + * parZip({ userProcessor.bind() }, { dataSource.bind() }) { userProcessor, ds -> * Service(ds, userProcessor) - * }.use { service -> service.processData() } + * } + * }.use { service -> service.processData() } * } - * //sampleEnd * ``` * * @@ -163,7 +160,6 @@ public sealed class Resource { * } * * suspend fun main(): Unit { - * //sampleStart * val dataSource = resource { * DataSource().also { it.connect() } * } release DataSource::close @@ -171,7 +167,6 @@ public sealed class Resource { * val res = dataSource * .use { ds -> "Using data source: ${ds.users()}" } * .also(::println) - * //sampleEnd * } * ``` * @@ -196,12 +191,14 @@ public sealed class Resource { } b } + is Allocate -> bracketCase(acquire, f, release) is Bind<*, *> -> Dsl { val any = source.bind() val ff = this@Resource.f as (Any?) -> Resource ff(any).bind() }.use(f) + is Defer -> resource().use(f) } @@ -240,7 +237,6 @@ public sealed class Resource { * } * * suspend fun main(): Unit { - * //sampleStart * val dataSource = resource { * DataSource().also { it.connect() } * } release DataSource::close @@ -252,7 +248,6 @@ public sealed class Resource { * * dataSource.flatMap(::database) * .use { println("Using database which uses dataSource") } - * //sampleEnd * } * ``` * @@ -301,7 +296,6 @@ public sealed class Resource { * suspend fun processData(): List = userProcessor.process(db) * } * - * //sampleStart * val userProcessor = resource { * UserProcessor().also(UserProcessor::start) * } release UserProcessor::shutdown @@ -315,7 +309,6 @@ public sealed class Resource { * Service(ds, userProcessor) * }.use { service -> service.processData() } * } - * //sampleEnd * ``` * * @@ -325,7 +318,7 @@ public sealed class Resource { public inline fun zip( b: Resource, c: Resource, - crossinline map: (A, B, C) -> D + crossinline map: (A, B, C) -> D, ): Resource = arrow.fx.coroutines.continuations.resource { map(bind(), b.bind(), c.bind()) @@ -335,18 +328,18 @@ public sealed class Resource { b: Resource, c: Resource, d: Resource, - crossinline map: (A, B, C, D) -> E + crossinline map: (A, B, C, D) -> E, ): Resource = arrow.fx.coroutines.continuations.resource { map(bind(), b.bind(), c.bind(), d.bind()) } - public inline fun zip( + public inline fun zip( b: Resource, c: Resource, d: Resource, e: Resource, - crossinline map: (A, B, C, D, E) -> G + crossinline map: (A, B, C, D, E) -> G, ): Resource = arrow.fx.coroutines.continuations.resource { map(bind(), b.bind(), c.bind(), d.bind(), e.bind()) @@ -358,7 +351,7 @@ public sealed class Resource { d: Resource, e: Resource, f: Resource, - crossinline map: (A, B, C, D, E, F) -> G + crossinline map: (A, B, C, D, E, F) -> G, ): Resource = arrow.fx.coroutines.continuations.resource { map(bind(), b.bind(), c.bind(), d.bind(), e.bind(), f.bind()) @@ -371,7 +364,7 @@ public sealed class Resource { e: Resource, f: Resource, g: Resource, - crossinline map: (A, B, C, D, E, F, G) -> H + crossinline map: (A, B, C, D, E, F, G) -> H, ): Resource = arrow.fx.coroutines.continuations.resource { map(bind(), b.bind(), c.bind(), d.bind(), e.bind(), f.bind(), g.bind()) @@ -385,7 +378,7 @@ public sealed class Resource { f: Resource, g: Resource, h: Resource, - crossinline map: (A, B, C, D, E, F, G, H) -> I + crossinline map: (A, B, C, D, E, F, G, H) -> I, ): Resource = arrow.fx.coroutines.continuations.resource { map(bind(), b.bind(), c.bind(), d.bind(), e.bind(), f.bind(), g.bind(), h.bind()) @@ -400,7 +393,7 @@ public sealed class Resource { g: Resource, h: Resource, i: Resource, - crossinline map: (A, B, C, D, E, F, G, H, I) -> J + crossinline map: (A, B, C, D, E, F, G, H, I) -> J, ): Resource = arrow.fx.coroutines.continuations.resource { map(bind(), b.bind(), c.bind(), d.bind(), e.bind(), f.bind(), g.bind(), h.bind(), i.bind()) @@ -416,7 +409,7 @@ public sealed class Resource { h: Resource, i: Resource, j: Resource, - crossinline map: (A, B, C, D, E, F, G, H, I, J) -> K + crossinline map: (A, B, C, D, E, F, G, H, I, J) -> K, ): Resource = arrow.fx.coroutines.continuations.resource { map(bind(), b.bind(), c.bind(), d.bind(), e.bind(), f.bind(), g.bind(), h.bind(), i.bind(), j.bind()) @@ -452,7 +445,6 @@ public sealed class Resource { * suspend fun processData(): List = userProcessor.process(db) * } * - * //sampleStart * val userProcessor = resource { * UserProcessor().also { it.start() } * } release UserProcessor::shutdown @@ -466,14 +458,13 @@ public sealed class Resource { * Service(ds, userProcessor) * }.use { service -> service.processData() } * } - * //sampleEnd * ``` * */ public fun parZip( ctx: CoroutineContext = Dispatchers.Default, fb: Resource, - f: suspend (A, B) -> C + f: suspend (A, B) -> C, ): Resource = arrow.fx.coroutines.continuations.resource { parZip(ctx, { this@Resource.bind() }, { fb.bind() }) { a, b -> f(a, b) } @@ -490,7 +481,7 @@ public sealed class Resource { public class Allocate( public val acquire: suspend () -> A, - public val release: suspend (A, ExitCase) -> Unit + public val release: suspend (A, ExitCase) -> Unit, ) : Resource() @Deprecated( @@ -507,6 +498,7 @@ public sealed class Resource { public companion object { @PublishedApi + @Deprecated("This will be removed from the binary in Arrow 2.0", level = DeprecationLevel.ERROR) internal val unit: Resource = just(Unit) /** @@ -519,19 +511,17 @@ public sealed class Resource { * suspend fun releaseResource(r: Int, exitCase: ExitCase): Unit = println("Releasing expensive resource: $r, exit: $exitCase") * * suspend fun main(): Unit { - * //sampleStart * val resource = Resource(::acquireResource, ::releaseResource) * resource.use { * println("Expensive resource under use! $it") * } - * //sampleEnd * } * ``` * */ public operator fun invoke( acquire: suspend () -> A, - release: suspend (A, ExitCase) -> Unit + release: suspend (A, ExitCase) -> Unit, ): Resource = Allocate(acquire, release) /** @@ -630,6 +620,14 @@ public infix fun Resource.releaseCase(release: suspend (A, ExitCase) -> U Resource({ a }, { _, ex -> release(a, ex) }).bind() } +@Deprecated("traverseResource is being renamed to traverse to simplify the Arrow API", ReplaceWith("traverse(f)")) +public inline fun Iterable.traverseResource(crossinline f: (A) -> Resource): Resource> = + arrow.fx.coroutines.continuations.resource { + map { a -> + f(a).bind() + } + } + /** * Traverse this [Iterable] and collects the resulting `Resource` of [f] into a `Resource>`. * @@ -647,12 +645,11 @@ public infix fun Resource.releaseCase(release: suspend (A, ExitCase) -> U * suspend fun fileToString(file: File): String = file.toString() * * suspend fun main(): Unit { - * //sampleStart * val res: List = listOf( * "data.json", * "user.json", * "resource.json" - * ).traverseResource { uri -> + * ).traverse { uri -> * resource { * openFile(uri) * } release { file -> @@ -661,13 +658,14 @@ public infix fun Resource.releaseCase(release: suspend (A, ExitCase) -> U * }.use { files -> * files.map { fileToString(it) } * } - * //sampleEnd * res.forEach(::println) * } * ``` * */ -public inline fun Iterable.traverseResource(crossinline f: (A) -> Resource): Resource> = +@OptIn(ExperimentalTypeInference::class) +@OverloadResolutionByLambdaReturnType +public inline fun Iterable.traverse(crossinline f: (A) -> Resource): Resource> = arrow.fx.coroutines.continuations.resource { map { a -> f(a).bind() @@ -676,7 +674,7 @@ public inline fun Iterable.traverseResource(crossinline f: (A) -> Reso /** * Sequences this [Iterable] of [Resource]s. - * [Iterable.map] and [sequence] is equivalent to [traverseResource]. + * [Iterable.map] and [sequence] is equivalent to [traverse]. * * ```kotlin * import arrow.fx.coroutines.* @@ -692,7 +690,6 @@ public inline fun Iterable.traverseResource(crossinline f: (A) -> Reso * suspend fun fileToString(file: File): String = file.toString() * * suspend fun main(): Unit { - * //sampleStart * val res: List = listOf( * "data.json", * "user.json", @@ -706,7 +703,6 @@ public inline fun Iterable.traverseResource(crossinline f: (A) -> Reso * }.sequence().use { files -> * files.map { fileToString(it) } * } - * //sampleEnd * res.forEach(::println) * } * ``` @@ -714,7 +710,35 @@ public inline fun Iterable.traverseResource(crossinline f: (A) -> Reso */ @Suppress("NOTHING_TO_INLINE") public inline fun Iterable>.sequence(): Resource> = - traverseResource(::identity) + traverse(::identity) + +/** + * Runs [Resource.use] and emits [A] of the resource + * + * ```kotlin + * import arrow.fx.coroutines.* + * + * fun Flow.writeAll(path: Path): Flow = + * Resource.fromCloseable { path.toFile().outputStream() } + * .asFlow() + * .flatMapConcat { writer -> byteFlow.map { writer.write(it) } } + * .flowOn(Dispatchers.IO) + * + * fun Path.readAll(): Flow = flow { + * path.useLines { lines -> emitAll(lines) } + * } + * + * Path("example.kt") + * .readAll() + * . + * ``` + */ +public fun Resource.asFlow(): Flow = + flow { + use { + emit(it) + } + } private class ResourceScopeImpl : ResourceScope { val finalizers: AtomicRef Unit>> = AtomicRef(emptyList()) @@ -735,6 +759,7 @@ private class ResourceScopeImpl : ResourceScope { Platform.composeErrors(e, e2)?.let { throw it } } }) + is Resource.Bind<*, *> -> { val dsl: suspend ResourceScope.() -> A = { val any = source.bind() @@ -743,34 +768,17 @@ private class ResourceScopeImpl : ResourceScope { } dsl(this@ResourceScopeImpl) } + is Resource.Defer -> resource().bind() } } -// Version that doesn't rethrow `CancellationException` because we need to run all finalizers regardless of CancellationException -private inline fun catchNel(f: () -> A): ValidatedNel = - try { - f().valid() - } catch (e: Throwable) { - e.invalidNel() - } - private suspend fun List Unit>.cancelAll( exitCase: ExitCase, - first: Throwable? = null -): Throwable? = traverse { f -> - catchNel { f(exitCase) } -}.fold({ - if (first != null) Platform.composeErrors(NonEmptyList(first, it)) - else Platform.composeErrors(it) -}, { first }) - -/** - * runs [Resource.use] and emits [A] of the resource - */ -public fun Resource.asFlow(): Flow = - flow { - use { - emit(it) - } - } + first: Throwable? = null, +): Throwable? = fold(first) { acc, finalizer -> + val other = kotlin.runCatching { finalizer(exitCase) }.exceptionOrNull() + other?.let { + acc?.apply { addSuppressed(other) } ?: other + } ?: acc +} diff --git a/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ResourceTest.kt b/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ResourceTest.kt index 9e46b749b71..f68e052ed3d 100644 --- a/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ResourceTest.kt +++ b/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ResourceTest.kt @@ -41,7 +41,7 @@ class ResourceTest : ArrowFxSpec( r(a).flatMap { r(it + b) } .use { it + 1 } shouldBe (a + b) + 1 - l.shouldContainExactly(a, a + b, - a - b, -a) + l.shouldContainExactly(a, a + b, -a - b, -a) } } @@ -90,39 +90,39 @@ class ResourceTest : ArrowFxSpec( Arb.list(Arb.int()), Arb.functionAToB(Arb.string()) ) { list, f -> - list.traverseResource { Resource.just(f(it)) } resourceShouldBe Resource.just(list.map(f)) + list.traverse { Resource.just(f(it)) } resourceShouldBe Resource.just(list.map(f)) } } - "traverseResource: map + sequence == traverse" { + "traverse: map + sequence == traverse" { checkAll( Arb.list(Arb.int()), Arb.string().map { { _: Int -> Resource.just(it) } } ) { list, f -> - list.traverseResource(f) resourceShouldBe list.map(f).sequence() + list.traverse(f) resourceShouldBe list.map(f).sequence() } } - "traverseResource: parallelComposition" { + "traverse: parallelComposition" { checkAll( Arb.list(Arb.int()), Arb.functionAToB(Arb.string()), Arb.functionAToB(Arb.string()) ) { list, f, g -> - val ff = list.traverseResource { Resource.just(f(it)) } - val gg = list.traverseResource { Resource.just(g(it)) } + val ff = list.traverse { Resource.just(f(it)) } + val gg = list.traverse { Resource.just(g(it)) } val result = ff.zip(gg).map { (a, b) -> a.zip(b) } - list.traverseResource { Resource.just(f(it) to g(it)) } resourceShouldBe result + list.traverse { Resource.just(f(it) to g(it)) } resourceShouldBe result } } - "traverseResource: leftToRight" { + "traverse: leftToRight" { checkAll(Arb.list(Arb.int())) { list -> - list.traverseResource { Resource.just(it) } + list.traverse { Resource.just(it) } .use(::identity) shouldBe list } } @@ -154,8 +154,8 @@ class ResourceTest : ArrowFxSpec( Resource({ CheckableAutoClose() }) { a, _ -> a.close() } "parZip - success" { - val all = (1..depth).traverseResource { closeable() }.parZip( - (1..depth).traverseResource { closeable() } + val all = (1..depth).traverse { closeable() }.parZip( + (1..depth).traverse { closeable() } ) { a, b -> a + b }.use { all -> all.also { all.forEach { it.started shouldBe true } } } diff --git a/arrow-libs/fx/arrow-fx-coroutines/src/jvmTest/kotlin/examples/example-resource-03.kt b/arrow-libs/fx/arrow-fx-coroutines/src/jvmTest/kotlin/examples/example-resource-03.kt index 389c744072b..d369bbbf7fd 100644 --- a/arrow-libs/fx/arrow-fx-coroutines/src/jvmTest/kotlin/examples/example-resource-03.kt +++ b/arrow-libs/fx/arrow-fx-coroutines/src/jvmTest/kotlin/examples/example-resource-03.kt @@ -2,6 +2,7 @@ package arrow.fx.coroutines.examples.exampleResource03 import arrow.fx.coroutines.* +import arrow.fx.coroutines.continuations.resource class UserProcessor { fun start(): Unit = println("Creating UserProcessor") @@ -29,7 +30,9 @@ val dataSource = resource { } release DataSource::close suspend fun main(): Unit { - userProcessor.parZip(dataSource) { userProcessor, ds -> + resource { + parZip({ userProcessor.bind() }, { dataSource.bind() }) { userProcessor, ds -> Service(ds, userProcessor) - }.use { service -> service.processData() } + } + }.use { service -> service.processData() } } diff --git a/arrow-libs/fx/arrow-fx-coroutines/src/jvmTest/kotlin/examples/example-resource-04.kt b/arrow-libs/fx/arrow-fx-coroutines/src/jvmTest/kotlin/examples/example-resource-04.kt index f9cfe3cc95e..f3f67d497cc 100644 --- a/arrow-libs/fx/arrow-fx-coroutines/src/jvmTest/kotlin/examples/example-resource-04.kt +++ b/arrow-libs/fx/arrow-fx-coroutines/src/jvmTest/kotlin/examples/example-resource-04.kt @@ -10,7 +10,6 @@ class DataSource { } suspend fun main(): Unit { - //sampleStart val dataSource = resource { DataSource().also { it.connect() } } release DataSource::close @@ -18,5 +17,4 @@ suspend fun main(): Unit { val res = dataSource .use { ds -> "Using data source: ${ds.users()}" } .also(::println) - //sampleEnd } diff --git a/arrow-libs/fx/arrow-fx-coroutines/src/jvmTest/kotlin/examples/example-resource-05.kt b/arrow-libs/fx/arrow-fx-coroutines/src/jvmTest/kotlin/examples/example-resource-05.kt index 588bf83ea37..24d8787a74b 100644 --- a/arrow-libs/fx/arrow-fx-coroutines/src/jvmTest/kotlin/examples/example-resource-05.kt +++ b/arrow-libs/fx/arrow-fx-coroutines/src/jvmTest/kotlin/examples/example-resource-05.kt @@ -16,7 +16,6 @@ class Database(private val database: DataSource) { } suspend fun main(): Unit { - //sampleStart val dataSource = resource { DataSource().also { it.connect() } } release DataSource::close @@ -28,5 +27,4 @@ suspend fun main(): Unit { dataSource.flatMap(::database) .use { println("Using database which uses dataSource") } - //sampleEnd } diff --git a/arrow-libs/fx/arrow-fx-coroutines/src/jvmTest/kotlin/examples/example-resource-08.kt b/arrow-libs/fx/arrow-fx-coroutines/src/jvmTest/kotlin/examples/example-resource-08.kt index 0c1964bb4bb..b1743db3bdf 100644 --- a/arrow-libs/fx/arrow-fx-coroutines/src/jvmTest/kotlin/examples/example-resource-08.kt +++ b/arrow-libs/fx/arrow-fx-coroutines/src/jvmTest/kotlin/examples/example-resource-08.kt @@ -7,10 +7,8 @@ suspend fun acquireResource(): Int = 42.also { println("Getting expensive resour suspend fun releaseResource(r: Int, exitCase: ExitCase): Unit = println("Releasing expensive resource: $r, exit: $exitCase") suspend fun main(): Unit { - //sampleStart val resource = Resource(::acquireResource, ::releaseResource) resource.use { println("Expensive resource under use! $it") } - //sampleEnd } diff --git a/arrow-libs/fx/arrow-fx-coroutines/src/jvmTest/kotlin/examples/example-resource-10.kt b/arrow-libs/fx/arrow-fx-coroutines/src/jvmTest/kotlin/examples/example-resource-10.kt index 1430c7ef2b7..57e567884b4 100644 --- a/arrow-libs/fx/arrow-fx-coroutines/src/jvmTest/kotlin/examples/example-resource-10.kt +++ b/arrow-libs/fx/arrow-fx-coroutines/src/jvmTest/kotlin/examples/example-resource-10.kt @@ -14,12 +14,11 @@ suspend fun closeFile(file: File): Unit = file.close() suspend fun fileToString(file: File): String = file.toString() suspend fun main(): Unit { - //sampleStart val res: List = listOf( "data.json", "user.json", "resource.json" - ).traverseResource { uri -> + ).traverse { uri -> resource { openFile(uri) } release { file -> @@ -28,6 +27,5 @@ suspend fun main(): Unit { }.use { files -> files.map { fileToString(it) } } - //sampleEnd res.forEach(::println) } diff --git a/arrow-libs/fx/arrow-fx-coroutines/src/jvmTest/kotlin/examples/example-resource-11.kt b/arrow-libs/fx/arrow-fx-coroutines/src/jvmTest/kotlin/examples/example-resource-11.kt index 8bc906f38b3..3987d148522 100644 --- a/arrow-libs/fx/arrow-fx-coroutines/src/jvmTest/kotlin/examples/example-resource-11.kt +++ b/arrow-libs/fx/arrow-fx-coroutines/src/jvmTest/kotlin/examples/example-resource-11.kt @@ -14,7 +14,6 @@ suspend fun closeFile(file: File): Unit = file.close() suspend fun fileToString(file: File): String = file.toString() suspend fun main(): Unit { - //sampleStart val res: List = listOf( "data.json", "user.json", @@ -28,6 +27,5 @@ suspend fun main(): Unit { }.sequence().use { files -> files.map { fileToString(it) } } - //sampleEnd res.forEach(::println) }