Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add guides on scan and parEvalMap. #106

Merged
merged 3 commits into from
Nov 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/reference/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
59 changes: 59 additions & 0 deletions docs/reference/parEvalMap.md
Original file line number Diff line number Diff line change
@@ -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
}
72 changes: 72 additions & 0 deletions docs/reference/scan.md
Original file line number Diff line number Diff line change
@@ -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
}
132 changes: 132 additions & 0 deletions examples/src/main/scala/docs/reference/parEvalMap.scala
Original file line number Diff line number Diff line change
@@ -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")
)
}

}
101 changes: 101 additions & 0 deletions examples/src/main/scala/docs/reference/scan.scala
Original file line number Diff line number Diff line change
@@ -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")
)
}
}