diff --git a/docs/reference/README.md b/docs/reference/README.md index d1beb8b..20a73c3 100644 --- a/docs/reference/README.md +++ b/docs/reference/README.md @@ -14,9 +14,11 @@ Learn [how to read aquascapes](../how-to-read-the-diagrams.md), then learn about - [take, takeWhile, takeThrough, takeRight](take.md) - [drop, dropWhile, dropThrough, dropLast, dropLastIf](drop.md) - [filter, filterNot, filterWithPrevious, changes, mapFilter](filter.md) + - [scan, scan1, scanMap, scanMonoid](scan.md) - [evalMap, evalTap, evalMapChunk, evalTapChunk](evalMap.md) - [bracket, bracketCase, resource, onFinalize, onFinalizeCase](bracket.md) - [merge, mergeHaltL, mergeHaltR](merge.md) + - [parEvalMap, parEvalMapUnordered, parEvalMapUnbounded](parEvalMap.md) - [flatMap](flatMap.md) - [append, `++`](append.md) - [raiseError, handleError, handleErrorWith, attempt](errors.md) diff --git a/docs/reference/parEvalMap.md b/docs/reference/parEvalMap.md new file mode 100644 index 0000000..b7c3ab4 --- /dev/null +++ b/docs/reference/parEvalMap.md @@ -0,0 +1,59 @@ +{% + pageid = DocsReferenceParEvalMap +%} + +# parEvalMap + +This page is a reference guide for the `parEvalMap` family of operators. It describes: + + - The basic behaviour of `parEvalMap`, `parEvalMapUnbounded` and `parEvalMapUnordered`. + - How `parEvalMap` operators destroy chunks. + +## Behaviour + +The `parEvalMap` family of operators are used to evaluate effects concurrently. + +### parEvalMapUnbounded + +`parEvalMapUnbounded` pulls an element from its input stream, constructs an effect for it, then evaluates it while pulling and constructs the subsequent effects. It evaluates the effects concurrently. + +In the following example, the elements `a`, `b` and `c` are pulled from the input stream. An `IO.sleep(1.second)` effect is evaluated for each of them. The effects are evaluated concurrently, so the total time taken is one second. + +@:example(parEvalMapUnbounded) { + drawChunked = false +} + +### parEvalMap + +`parEvalMap(n)` evaluates at most `n` effects concurrently. Experiment with the number of concurrent effects below. + +@:exampleWithInput(parEvalMapConcurrency) { + drawChunked = false +} + +#### Output order + +The elements outputted by `parEvalMap` and `parEvalMapUnbounded` are in the same order as their corresponding input. + +In the following example, the effect for the first element `3` takes longer to be evaluated than that of the second element `2`. The second result is only outputted after the first result, even though it finishes before it. + +@:example(parEvalMapOrder) { + drawChunked = false +} + +### parEvalMapUnordered + +`parEvalMapUnordered` is similar to `parEvalMap`, but it outputs elements in the order that their effect evaluation completes. + +@:example(parEvalMapUnordered) { + drawChunked = false +} + +# Chunk preservation + +The `parEvalMap` family of operators do not preserve chunks. They always output singleton chunks. + +@:example(parEvalMapSingletonChunks) { + suffix = chunked + drawChunked = true +} diff --git a/docs/reference/scan.md b/docs/reference/scan.md new file mode 100644 index 0000000..40d26bd --- /dev/null +++ b/docs/reference/scan.md @@ -0,0 +1,72 @@ +{% + pageid = DocsReferenceScan +%} + +# scan + +This page is a reference guide for the `scan` family of operators. It describes: + + - The behaviour of `scan`, `scan1`, `scanMap` and `scanMonoid`. + - How `scan` operators propagate chunks. + +As the `scan` family of operators have normal error propagation and finalizer handling, these are not described. + +## Behaviour + +The `scan` family of operators perform a left fold over an input stream and output intermediate results. + +### scan + +The `scan` operator pulls an element from its input stream, evaluates a function with its previous result, then outputs the result. It is analogous to `scan` on lists. + +The following example scans over an input stream of characters to produce a string. A character `ch` is pulled and combined with an accumulated string `str` with the `s"$str-$ch"` function. + +@:example(scan) { + drawChunked = false +} + +### scan1 + +The `scan1` operator is similar to `scan`, but uses the first element pulled from the input stream to evaluate the result. + +@:example(scan1) { + drawChunked = false +} + + +### scanMonoid + +The `scanMonoid` operator scans over its input with a `Monoid` typeclass instance. + +The following example uses the `String` monoid instance. The empty element of the instance `""` is outputted first. The `combine` function of the instance concatenates two strings. + +@:example(scanMonoid) { + drawChunked = false +} + +### scanMap + +The `scanMap` operator maps over its input then scans with a `Monoid` typeclass instance. `input.scanMap(f)` is equivalent to `input.map(f).scanMonoid`. + +@:example(scanMap) { + drawChunked = false +} + +## Chunk propagation + +## scan + +The `scan` operator preserves the chunks of its input. The first argument to `scan` is outputted as a singleton chunk. + +@:example(scanChunkPropagation) { + drawChunked = true +} + +## scan1 + +`scan1` preserves the chunks of its input, with the exception of the first chunk. The first element of the first chunk is outputted as a singleton chunk. + +@:example(scan1) { + suffix = chunked + drawChunked = true +} diff --git a/examples/src/main/scala/docs/reference/parEvalMap.scala b/examples/src/main/scala/docs/reference/parEvalMap.scala new file mode 100644 index 0000000..8780134 --- /dev/null +++ b/examples/src/main/scala/docs/reference/parEvalMap.scala @@ -0,0 +1,132 @@ +/* + * Copyright 2023 Zainab Ali + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package docs.reference + +import aquascape.* +import aquascape.examples.* +import cats.Show +import cats.effect.* +import cats.effect.IO +import fs2.* +import scala.concurrent.duration.* + +import scala.scalajs.js.annotation.JSExport +import scala.scalajs.js.annotation.JSExportTopLevel + +@JSExportTopLevel("DocsReferenceParEvalMap") +object parEvalMap { + def concurrencyInputBox(max: Int): InputBox[Int] = InputBox.int( + labelText = "n (concurrent effects)", + defaultValue = 2, + min = 1, + max = max + ) + + + @JSExport + val parEvalMapUnbounded = new Example { + def apply(using Scape[IO]): StreamCode = + code( + Stream('a', 'b', 'c') + .stage("Stream('a','b','c')", "upstream") + .fork("root", "upstream") + .parEvalMapUnbounded(ch => IO.sleep(1.second).as(ch).trace()) + .stage("parEvalMapUnbounded(…)") + .compile + .toList + .compileStage("compile.toList") + ) + } + + @JSExport + val parEvalMapConcurrency = new ExampleWithInput[Int] { + val inputBox: InputBox[Int] = concurrencyInputBox(4) + def apply(n: Int)(using Scape[IO]): StreamCode = + code( + Stream('a', 'b', 'c', 'd') + .stage("Stream('a','b','c', 'd')", "upstream") + .fork("root", "upstream") + .parEvalMap(n)(ch => IO.sleep(1.second).as(ch).trace()) + .stage(s"parEvalMap($n)(…)") + .compile + .toList + .compileStage("compile.toList") + ) + } + + @JSExport + val parEvalMapOrder = new Example { + def apply(using Scape[IO]): StreamCode = + code( + Stream(3, 2, 1) + .stage("Stream(3, 2, 1)", "upstream") + .fork("root", "upstream") + .parEvalMap(2)(n => IO.sleep(n.second).as(n).trace()) + .stage("parEvalMap(2)(…)") + .compile + .toList + .compileStage("compile.toList") + ) + } + + @JSExport + val parEvalMapInputSequential = new Example { + def apply(using Scape[IO]): StreamCode = + code( + Stream('a', 'b', 'c') + .spaced[IO](1.second) + .stage("Stream('a', 'b', 'c').spaced(1s)", "upstream") + .fork("root", "upstream") + .parEvalMap(2)(ch => IO.sleep(1.second).as(ch).trace()) + .stage("parEvalMap(2)(…)") + .compile + .toList + .compileStage("compile.toList") + ) + } + + @JSExport + val parEvalMapUnordered = new Example { + def apply(using Scape[IO]): StreamCode = + code( + Stream(4, 2, 1) + .stage("Stream(4, 2, 1)", "upstream") + .fork("root", "upstream") + .parEvalMapUnordered(2)(n => IO.sleep(n.second).as(n).trace()) + .stage("parEvalMapUnordered(2)(…)") + .compile + .toList + .compileStage("compile.toList") + ) + } + + @JSExport + val parEvalMapSingletonChunks = new Example { + def apply(using Scape[IO]): StreamCode = + code( + Stream('a', 'b', 'c') + .stage("Stream('a','b','c')", "upstream") + .fork("root", "upstream") + .parEvalMap(2)(ch => IO.sleep(1.second).as(ch).trace()) + .stage("parEvalMap(2)(…)") + .compile + .toList + .compileStage("compile.toList") + ) + } + +} diff --git a/examples/src/main/scala/docs/reference/scan.scala b/examples/src/main/scala/docs/reference/scan.scala new file mode 100644 index 0000000..a4b0c7e --- /dev/null +++ b/examples/src/main/scala/docs/reference/scan.scala @@ -0,0 +1,101 @@ +/* + * Copyright 2023 Zainab Ali + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package docs.reference + +import aquascape.* +import aquascape.examples.* +import cats.Show +import cats.effect.* +import cats.effect.IO +import fs2.* + +import scala.scalajs.js.annotation.JSExport +import scala.scalajs.js.annotation.JSExportTopLevel + +@JSExportTopLevel("DocsReferenceScan") +object scan { + + @JSExport + val scan = new Example { + def apply(using Scape[IO]): StreamCode = + code( + Stream('a', 'b', 'c') + .stage("Stream('a', 'b', 'c')") + .scan("")((str, ch) => s"$str-$ch") + .stage("scan(…)") + .compile + .toList + .compileStage("compile.toList") + ) + } + + @JSExport + val scan1 = new Example { + def apply(using Scape[IO]): StreamCode = + code( + Stream("a", "b", "c") + .stage("""Stream("a", "b", "c")""") + .scan1((str, ch) => s"$str-$ch") + .stage("scan1(…)") + .compile + .toList + .compileStage("compile.toList") + ) + } + + @JSExport + val scanMonoid = new Example { + def apply(using Scape[IO]): StreamCode = + code( + Stream("a", "b", "c") + .stage("""Stream("a", "b", "c")""") + .scanMonoid + .stage("scanMonoid") + .compile + .toList + .compileStage("compile.toList") + ) + } + + @JSExport + val scanMap = new Example { + def apply(using Scape[IO]): StreamCode = + code( + Stream("a", "b", "c") + .stage("""Stream("a", "b", "c")""") + .scanMap(_.toUpperCase) + .stage("scanMap(_.toUpperCase)") + .compile + .toList + .compileStage("compile.toList") + ) + } + + @JSExport + val scanChunkPropagation = new Example { + def apply(using Scape[IO]): StreamCode = + code( + (Stream("a", "b") ++ Stream("c")) + .stage("Stream(…) ++ Stream(…)") + .scan("")((str, ch) => s"$str-$ch") + .stage("scan(…)") + .compile + .toList + .compileStage("compile.toList") + ) + } +}