Skip to content

Commit a27df6d

Browse files
committed
New rules
1 parent eb4f753 commit a27df6d

15 files changed

+1730
-12
lines changed
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
---
2+
description:
3+
globs:
4+
alwaysApply: true
5+
---
6+
# Ox Library Overview
7+
8+
Ox is a Scala 3 library for **safe direct-style streaming, concurrency and resiliency** on the JVM. Requires JDK 21+ and Scala 3.
9+
10+
## Key Concepts
11+
12+
**Direct-Style Programming**: Write blocking-style code using virtual threads instead of async/await or reactive streams. This provides readable, debuggable code without callback hell.
13+
14+
**Dual Error Handling**:
15+
- Use **exceptions** for bugs and unexpected situations
16+
- Use **Either values** for application logic errors with `.ok()` unwrapping
17+
18+
**Structured Concurrency**: All concurrent operations happen within high-level operations or low-level scopes that ensure proper cleanup and resource management.
19+
20+
**Streaming with Flows**: Use `Flow[T]` for lazy, composable, reactive-streams compatible data transformation pipelines with built-in concurrency control.
21+
22+
## Common Patterns
23+
24+
```scala
25+
// High-level concurrency
26+
val result = par(computation1, computation2)
27+
val winner = raceSuccess(comp1, comp2)
28+
29+
// Streaming transformations
30+
Flow.fromValues(1, 2, 3)
31+
.mapPar(4)(process)
32+
.filter(_ > 0)
33+
.runForeach(println)
34+
35+
// Error handling with Either
36+
val result: Either[String, Int] = either:
37+
val user = lookupUser(id).ok()
38+
val org = lookupOrg(id).ok()
39+
calculateResult(user, org)
40+
41+
// Structured concurrency scopes
42+
supervised:
43+
val f1 = fork { longRunningTask1() }
44+
val f2 = fork { longRunningTask2() }
45+
(f1.join(), f2.join())
46+
```
47+
48+
Ox emphasizes safety, composability and readability while providing high performance through virtual threads.
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
---
2+
description: Prefer high-level operations like `par()`, `race()`, and `timeout()` for concurrent programming instead of manual fork management. These operations handle error propagation, interruption, and resource cleanup automatically. Use `.mapPar`, `.filterPar` and `.collectPar` on large collections for added performance.
3+
globs:
4+
alwaysApply: false
5+
---
6+
# High-Level Concurrency Operations
7+
8+
**Prefer high-level concurrency operations** over manual fork management. These operations automatically handle error propagation, interruption, and resource cleanup.
9+
10+
## Core Operations
11+
12+
**Parallel execution** with `par()`:
13+
```scala
14+
import ox.{par, sleep}
15+
import scala.concurrent.duration.*
16+
17+
val result: (Int, String) = par(
18+
{ sleep(2.seconds); 1 },
19+
{ sleep(1.second); "done" }
20+
)
21+
```
22+
23+
**Racing computations** with `race()` and `raceSuccess()`:
24+
```scala
25+
import ox.{raceSuccess, timeout}
26+
27+
val winner = raceSuccess(
28+
computation1,
29+
timeout(1.second)(computation2)
30+
)
31+
```
32+
33+
**Timeouts** with automatic interruption:
34+
```scala
35+
import ox.{timeout, either}
36+
37+
val result = either.catching:
38+
timeout(5.seconds)(longRunningTask())
39+
```
40+
41+
## Collection Operations
42+
43+
**Use parallel collection operations** for improved performance:
44+
```scala
45+
// Instead of manual forking
46+
val results = items.mapPar(4)(process) // parallel map
47+
val filtered = items.filterPar(8)(predicate) // parallel filter
48+
val collected = items.collectPar(2)(partial) // parallel collect
49+
```
50+
51+
## Error Handling
52+
53+
High-level operations **automatically handle failures**:
54+
- Failed computations interrupt others
55+
- Resources are properly cleaned up
56+
- Original exceptions are preserved and re-thrown
57+
- `parEither` variant supports application errors
58+
59+
**Good**: Using high-level operations
60+
```scala
61+
val result = parEither(
62+
{ sleep(100.millis); Right("ok") },
63+
{ sleep(200.millis); Left("error") }
64+
)
65+
```
66+
67+
**Avoid**: Manual fork management for simple parallel operations
68+
```scala
69+
// Don't do this for simple cases
70+
supervised:
71+
val f1 = fork(computation1)
72+
val f2 = fork(computation2)
73+
(f1.join(), f2.join())
74+
```
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
---
2+
description: Use exceptions for bugs/unexpected situations and use `Either` values for application logic errors. Use Rust-like `either` blocks with `.ok()` to unwrap `Either` values with automatic short-circuiting. Combine different error types using union types (`Either[Int | Long, String]`) and avoid nested `either` blocks in the same scope.
3+
globs:
4+
alwaysApply: false
5+
---
6+
# Dual Error Handling
7+
8+
Ox uses **two channels for error handling**:
9+
1. **Exceptions**: for bugs, unexpected situations, Java library integration
10+
2. **Either values**: for application logic errors (typed, explicit)
11+
12+
## Either Blocks with `.ok()` Unwrapping
13+
14+
**Use `either` blocks** for Rust-like error handling with automatic short-circuiting:
15+
16+
```scala
17+
import ox.either
18+
import ox.either.ok
19+
20+
case class User()
21+
case class Organization()
22+
23+
def lookupUser(id: Int): Either[String, User] = ???
24+
def lookupOrganization(id: Int): Either[String, Organization] = ???
25+
26+
// Automatic short-circuiting on first Left value
27+
val result: Either[String, Assignment] = either:
28+
val user = lookupUser(1).ok() // unwraps Right, or short-circuits on Left
29+
val org = lookupOrganization(2).ok()
30+
Assignment(user, org)
31+
```
32+
33+
## Union Types for Multiple Error Types
34+
35+
**Combine different error types** using union types:
36+
37+
```scala
38+
val v1: Either[Int, String] = ???
39+
val v2: Either[Long, String] = ???
40+
41+
val result: Either[Int | Long, String] = either:
42+
v1.ok() ++ v2.ok() // Error type becomes Int | Long
43+
```
44+
45+
## Converting Exceptions to Either
46+
47+
**Convert exception-throwing code** using `either.catching`:
48+
49+
```scala
50+
val result: Either[Throwable, String] = either.catching:
51+
riskyOperation() // catches non-fatal exceptions
52+
```
53+
54+
## Important: Avoid Nested `either` Blocks
55+
56+
**Don't nest `either` blocks** in the same scope - this can cause surprising behavior after refactoring:
57+
58+
```scala
59+
// BAD: Nested either blocks
60+
val outerResult: Either[Exception, Unit] = either:
61+
val innerResult: Either[String, Int] = either: // DON'T DO THIS
62+
returnsEither.ok() // Which either block does this target?
63+
// ...
64+
()
65+
66+
// GOOD: Extract to separate function
67+
def innerLogic(): Either[String, Int] = either:
68+
returnsEither.ok()
69+
70+
val outerResult: Either[Exception, Unit] = either:
71+
val innerResult = innerLogic()
72+
()
73+
```
74+
75+
## When to Use Each Approach
76+
77+
**Use exceptions for**:
78+
- Programming bugs
79+
- Unexpected system failures
80+
- Java library integration
81+
82+
**Use Either for**:
83+
- Expected application errors
84+
- Validation failures
85+
- Business logic errors
86+
- When you want explicit error types in signatures
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
---
2+
description: Use Flows for functional-style streaming transformations with methods like `map`, `mapPar`, `filter`, `groupBy` and many others. Flows are lazy, composable, and manage concurrency declaratively and efficiently. Flows only start processing data when run.
3+
globs:
4+
alwaysApply: false
5+
---
6+
# Streaming: Prefer Flows Over Channels
7+
8+
**Use `Flow[T]` for streaming data transformations** instead of low-level channel operations. Flows provide a functional, composable API with built-in concurrency management.
9+
10+
## Flow Characteristics
11+
12+
**Flows are lazy**: No processing happens until `.run*()` methods are called
13+
```scala
14+
import ox.flow.Flow
15+
16+
// This just describes the pipeline - nothing executes yet
17+
val pipeline = Flow.fromValues(1, 2, 3)
18+
.map(_ * 2)
19+
.filter(_ > 2)
20+
21+
// Processing happens here
22+
val result = pipeline.runToList() // List(4, 6)
23+
```
24+
25+
## Creating Flows
26+
27+
**Multiple ways to create flows**:
28+
```scala
29+
import ox.flow.Flow
30+
import scala.concurrent.duration.*
31+
32+
// From values
33+
Flow.fromValues(1, 2, 3)
34+
35+
// Infinite flows
36+
Flow.tick(1.second, "heartbeat")
37+
Flow.iterate(0)(_ + 1) // natural numbers
38+
39+
// From channels
40+
Flow.fromSource(channel)
41+
42+
// Custom emission logic
43+
Flow.usingEmit: emit =>
44+
emit(1)
45+
for i <- 4 to 50 do emit(i)
46+
if condition() then emit(42)
47+
```
48+
49+
## Functional Transformations
50+
51+
**Rich transformation API** similar to Scala collections:
52+
```scala
53+
Flow.fromValues(1, 2, 3, 4, 5)
54+
.map(_ * 2)
55+
.filter(_ % 4 == 0)
56+
.take(3)
57+
.zip(Flow.repeat("item"))
58+
.interleave(Flow.fromValues((0, "other")))
59+
.runForeach(println)
60+
```
61+
62+
## Built-in Concurrency Management
63+
64+
**Flows handle concurrency declaratively**:
65+
```scala
66+
// Parallel processing with controlled concurrency
67+
Flow.fromValues(urls)
68+
.mapPar(4)(sendHttpRequest) // max 4 concurrent requests
69+
.filter(_.isSuccess)
70+
.runDrain()
71+
72+
// Asynchronous boundaries with buffering
73+
Flow.fromSource(inputChannel)
74+
.buffer(capacity = 100) // explicit async boundary
75+
.mapPar(8)(process)
76+
.runToChannel()
77+
```
78+
79+
## When to Use Channels Directly
80+
81+
**Use low-level channels only for**:
82+
- Go-like inter-thread communication patterns
83+
- Custom coordination with `select` operations
84+
- Bridging callback-based APIs
85+
- Building custom flow operations
86+
87+
**Good**: Flow-based streaming
88+
```scala
89+
Flow.fromInputStream(inputStream)
90+
.linesUtf8
91+
.mapPar(parallelism)(processLine)
92+
.runForeach(println)
93+
```
94+
95+
**Avoid**: Manual channel coordination for simple transformations
96+
```scala
97+
// Don't do this for simple transformations
98+
val ch1 = Channel.buffered[String](mdc:10)
99+
val ch2 = Channel.buffered[Int](mdc:10)
100+
fork:
101+
ch1.drain(): line =>
102+
ch2.send(line.length)
103+
ch2.done()
104+
```
105+
106+
Flows provide **safety, composability, and performance** with a familiar functional API.

0 commit comments

Comments
 (0)