From 07653e86b1ca982c0ed8367bfad250643dc163f2 Mon Sep 17 00:00:00 2001 From: Magnus Madsen Date: Mon, 12 Oct 2026 12:41:21 +0200 Subject: [PATCH] docs: use one notation for effect annotations Apply a single style to every effect annotation in the book: - A single effect has no braces: `\ IO`, `\ ef`, `\ r`. - Two or more concrete effects use set braces with inner spaces: `\ { Http, IO }`. Concrete effects are never joined with `+`. - `IO` comes last. - Effect variables never go inside braces. They come first, joined by `+`, followed by one bare effect or one braced set: `\ ef + IO`, `\ r + { Clock, Http, IO }`. - Pure functions carry no annotation; `\ {}` appears only where the text is explicitly about the empty effect set. The quoted compiler error in the polymorphic-effects and common-problems chapters was regenerated with Flix 0.78.0 so its caret matches the rewritten source line. Comment carets in the effect-polymorphism chapter were realigned. Refs #283 Co-Authored-By: Claude Fable 5.1 --- src/common-problems.md | 8 +++--- src/concurrency.md | 10 ++++---- src/default-handlers.md | 6 ++--- src/effect-oriented-programming.md | 10 ++++---- src/effect-polymorphism.md | 40 +++++++++++++++--------------- src/effects-and-handlers.md | 6 ++--- src/for-llms.md | 6 ++--- src/introduction.md | 8 +++--- src/java-collections.md | 10 ++++---- src/logger.md | 2 +- src/main.md | 4 +-- src/mutable-collections.md | 2 +- src/polymorphic-effects.md | 8 +++--- src/test-framework.md | 2 +- 14 files changed, 61 insertions(+), 61 deletions(-) diff --git a/src/common-problems.md b/src/common-problems.md index 578e2c1c..046e835a 100644 --- a/src/common-problems.md +++ b/src/common-problems.md @@ -126,7 +126,7 @@ eff Emit[t] { def emit(x: t): Unit } -def f(): Unit \ Emit[Int32] + Emit[String] = +def f(): Unit \ { Emit[Int32], Emit[String] } = Emit.emit(42); Emit.emit("Hello") ``` @@ -138,9 +138,9 @@ The Flix compiler reports: >> Mismatched type arguments for effect 'Emit': 'Int32' and 'String'. -5 | def f(): Unit \ Emit[Int32] + Emit[String] = - ^^^^^^^^^^^^ - mismatched effect type argument. +5 | def f(): Unit \ { Emit[Int32], Emit[String] } = + ^^^^^^^^^^^^ + mismatched effect type argument. The effect 'Emit' is used with different types for its 1st type parameter 't'. diff --git a/src/concurrency.md b/src/concurrency.md index 7e4c8759..aad3f8af 100644 --- a/src/concurrency.md +++ b/src/concurrency.md @@ -59,7 +59,7 @@ Here is an example of sending and receiving a message over a channel: ```flix -def main(): Int32 \ {Chan, NonDet, IO} = region rc { +def main(): Int32 \ { Chan, NonDet, IO } = region rc { let (tx, rx) = Channel.unbuffered(); spawn Channel.send(42, tx) @ rc; Channel.recv(rx) @@ -89,7 +89,7 @@ def meow(tx: Sender[String]): Unit \ Chan = def woof(tx: Sender[String]): Unit \ Chan = Channel.send("Woof!", tx) -def main(): Unit \ {Chan, NonDet, IO} = region rc { +def main(): Unit \ { Chan, NonDet, IO } = region rc { let (tx1, rx1) = Channel.buffered(1); let (tx2, rx2) = Channel.buffered(1); spawn meow(tx1) @ rc; @@ -115,7 +115,7 @@ We can achieve this with a _default case_ as shown below: ```flix -def main(): String \ {Chan, NonDet} = region rc { +def main(): String \ { Chan, NonDet } = region rc { let (_, rx1) = Channel.buffered(1); let (_, rx2) = Channel.buffered(1); select { @@ -146,12 +146,12 @@ a channel, but the `select` expression relies on giving up: ```flix -def slow(tx: Sender[String]): Unit \ {Chan, NonDet, IO} = +def slow(tx: Sender[String]): Unit \ { Chan, NonDet, IO } = let delay = Channel.timeout(60, Time.TimeUnit.Seconds); Channel.recv(delay); Channel.send("I am very slow", tx) -def main(): Unit \ {Chan, NonDet, IO} = region rc { +def main(): Unit \ { Chan, NonDet, IO } = region rc { let (tx, rx) = Channel.buffered(1); spawn slow(tx) @ rc; let timeout = Channel.timeout(5, Time.TimeUnit.Seconds); diff --git a/src/default-handlers.md b/src/default-handlers.md index a941a5fc..84374353 100644 --- a/src/default-handlers.md +++ b/src/default-handlers.md @@ -11,7 +11,7 @@ For example, we can write: use Sys.Env use Time.Clock -def main(): Unit \ {Clock, Env, Logger} = +def main(): Unit \ { Clock, Env, Logger } = let ts = Clock.currentTime(TimeUnit.Milliseconds); let os = Env.getOsName(); Logger.info("UNIX Timestamp: ${ts}"); @@ -64,7 +64,7 @@ We can use effects with default handlers in tests. For example: ```flix @Test -def myTest01(): Unit \ {Assert, Logger} = +def myTest01(): Unit \ { Assert, Logger } = Logger.info("Running test!"); Assert.assertEq(expected = 42, 42) ``` @@ -89,7 +89,7 @@ mod Emit { } } -def main(): Unit \ {Emit[Int32], IO} = +def main(): Unit \ { Emit[Int32], IO } = Emit.emit(42); println("Done") ``` diff --git a/src/effect-oriented-programming.md b/src/effect-oriented-programming.md index b0b486e8..e448b9ac 100644 --- a/src/effect-oriented-programming.md +++ b/src/effect-oriented-programming.md @@ -28,7 +28,7 @@ import java.io.BufferedReader; import java.io.InputStreamReader; import java.util.{Random => JRandom} -def getSecretNumber(): Int32 \ {NonDet, IO} = +def getSecretNumber(): Int32 \ { NonDet, IO } = let rnd = new JRandom(); rnd.nextInt() @@ -61,7 +61,7 @@ def gameLoop(secret: Int32): Unit \ IO = { } } -def main(): Unit \ {NonDet, IO} = +def main(): Unit \ { NonDet, IO } = let secret = getSecretNumber(); gameLoop(secret) ``` @@ -100,12 +100,12 @@ eff Terminal { def println(s: String): Unit } -def readAndParseGuess(): Result[String, Int32] \ {Guess} = +def readAndParseGuess(): Result[String, Int32] \ Guess = forM(g <- Guess.readGuess(); n <- Int32.parse(10, g) ) yield n -def gameLoop(secret: Int32): Unit \ {Guess, Terminal} = { +def gameLoop(secret: Int32): Unit \ { Guess, Terminal } = { Terminal.println("Enter a guess:"); match readAndParseGuess() { case Result.Ok(g) => @@ -121,7 +121,7 @@ def gameLoop(secret: Int32): Unit \ {Guess, Terminal} = { } } -def main(): Unit \ {NonDet, IO} = +def main(): Unit \ { NonDet, IO } = run { let secret = Secret.getSecret(); gameLoop(secret) diff --git a/src/effect-polymorphism.md b/src/effect-polymorphism.md index 464b535a..f4429a01 100644 --- a/src/effect-polymorphism.md +++ b/src/effect-polymorphism.md @@ -8,8 +8,8 @@ In Flix, we can express that a function is pure (i.e. has no side-effects): ```flix -def inc(x: Int32): Int32 \ { } = x + 1 - // ^^^ empty effect set +def inc(x: Int32): Int32 \ {} = x + 1 + // ^^ empty effect set ``` The `inc` function is _pure_ because its effect set is empty. When a function is @@ -26,8 +26,8 @@ def inc(x: Int32): Int32 = x + 1 In Flix, we can express that a function has a single effect: ```flix -def incAndPrint(x: Int32): Int32 \ {IO} = - let result = x + 1; // ^^^^ singleton effect set +def incAndPrint(x: Int32): Int32 \ IO = + let result = x + 1; // ^^ singleton effect set println(result); result ``` @@ -38,8 +38,8 @@ Here the `incAndPrint` function has the primitive `IO` effect. We can also express that a function has multiple effects: ```flix -def copyFile(src: File, dst: File): Unit \ {FsRead, FsWrite, IO} = ... - // ^^^^^^^^^^^^^^^^^^^^ multiple effects +def copyFile(src: File, dst: File): Unit \ { FsRead, FsWrite, IO } = ... + // ^^^^^^^^^^^^^^^^^^^^^^^ multiple effects ``` Here the `copyFile` function has three primitive effects: `FsRead`, `FsWrite`, @@ -48,8 +48,8 @@ and `IO`. In Flix, we can express a function that has a heap effect: ```flix -def nth(i: Int32, a: Array[t, r]): Option[a] \ {r} = .... - // ^^^ heap effect +def nth(i: Int32, a: Array[t, r]): Option[a] \ r = .... + // ^ heap effect ``` Here the `nth` function has a heap effect in the region `r`. @@ -59,8 +59,8 @@ We can also write functions that mix different effects: ```flix use Time.Clock -def strange(a: Array[t, r]): Unit \ {r, Clock, Http, IO} - // ^^^^^^^^^^^^^^^^^^^ a mixture of effects +def strange(a: Array[t, r]): Unit \ r + { Clock, Http, IO } + // ^^^^^^^^^^^^^^^^^^^^^^^ a mixture of effects ``` This function has a heap effect `r` and three effects: `Clock`, `Http`, and `IO`. @@ -72,8 +72,8 @@ When we write higher-order functions, we must think carefully about their effect For example, we can write a higher-order function `Set.exists`: ```flix -def exists(f: a -> Bool \ { }, s: Set[a]): Bool = ... - ^^^ +def exists(f: a -> Bool \ {}, s: Set[a]): Bool = ... + ^^ ``` Here the `exists` function enforces the predicate function `f` to be pure. Why @@ -100,8 +100,8 @@ pure (i.e. has no effects) then the call to `map` will be pure. If `f` has the ```flix -List.map(x -> x + 1, l) // has the { } effect (i.e., is pure) -List.map(x -> {println(x); x + 1}, l) // has the { IO } effect +List.map(x -> x + 1, l) // has the {} effect (i.e., is pure) +List.map(x -> {println(x); x + 1}, l) // has the IO effect ``` A higher-order function that takes multiple function arguments may combine their @@ -171,9 +171,9 @@ For example, if we write: if (???) { x -> x + 1 } else { x -> {println(x); x + 1}} ``` -The first branch should have type `Int32 -> Int32 \ { }` (i.e. it is pure) -whereas the second branch has type `Int32 -> Int32 \ { IO }`. Without -sub-effecting these two types are incompatible because `{ } != { IO }`. However, +The first branch should have type `Int32 -> Int32 \ {}` (i.e. it is pure) +whereas the second branch has type `Int32 -> Int32 \ IO`. Without +sub-effecting these two types are incompatible because `{} != IO`. However, because of sub-effecting, Flix gives the first branch the type `Int32 -> Int32 \ ef` for some fresh effect variable `ef`. This allows type inference to _widen_ the effect of the first branch to `IO`. Hence the compiler is able to type check @@ -201,7 +201,7 @@ For example, we can define the trait: ```flix trait Foo[t] { - def f(x: t): Bool \ { IO } + def f(x: t): Bool \ IO } ``` @@ -214,8 +214,8 @@ instance Foo[Int32] { ``` The declared effect of `f` is `IO`, but here the implementation of `f` is pure -(i.e., it has the empty effect set `{ }`). The program still type checks because -`{ }` can be widened to `IO`. +(i.e., it has the empty effect set `{}`). The program still type checks because +`{}` can be widened to `IO`. Flix, however, does not allow sub-effecting for top-level functions. diff --git a/src/effects-and-handlers.md b/src/effects-and-handlers.md index dd3320a4..a580e7af 100644 --- a/src/effects-and-handlers.md +++ b/src/effects-and-handlers.md @@ -93,7 +93,7 @@ eff HourOfDay { def getCurrentHour(): Int32 } -def greeting(): String \ {HourOfDay} = +def greeting(): String \ HourOfDay = let h = HourOfDay.getCurrentHour(); if (h <= 12) "Good morning" @@ -136,7 +136,7 @@ eff Say { def say(s: String): Unit } -def greeting(): Unit \ {Ask, Say} = +def greeting(): Unit \ { Ask, Say } = let name = Ask.ask(); Say.say("Hello Mr. ${name}") @@ -174,7 +174,7 @@ eff Exc { def raise(m: String): Void } -def drunkFlip(): String \ {Amb, Exc} = { +def drunkFlip(): String \ { Amb, Exc } = { if (Amb.flip()) { let heads = Amb.flip(); if (heads) "heads" else "tails" diff --git a/src/for-llms.md b/src/for-llms.md index 60da3de1..be8bc407 100644 --- a/src/for-llms.md +++ b/src/for-llms.md @@ -260,11 +260,11 @@ use `run`/`with handler` blocks instead. ❌ **Incorrect (manual CPS with nested callbacks):** ``` -def handleSleep(f: Unit -> a \ { Sleep, ef }): a \ ef + IO = ... +def handleSleep(f: Unit -> a \ ef + Sleep): a \ ef + IO = ... -def withLogging(f: Unit -> a \ { Sleep, ef }): a \ ef + { Sleep, Logger } = ... +def withLogging(f: Unit -> a \ ef + Sleep): a \ ef + { Sleep, Logger } = ... -def withJitter(factor: Float64, f: Unit -> a \ { Sleep, ef }): a \ ef + { Sleep, Random } = ... +def withJitter(factor: Float64, f: Unit -> a \ ef + Sleep): a \ ef + { Sleep, Random } = ... def main(): Unit \ { Logger, Random, Sleep, IO } = handleSleep(() -> diff --git a/src/introduction.md b/src/introduction.md index 374d1a7e..f95fd866 100644 --- a/src/introduction.md +++ b/src/introduction.md @@ -149,7 +149,7 @@ eff MyTime { def getCurrentHour(): Int32 } -def sayGreeting(name: String): Unit \ {MyPrint, MyTime} = { +def sayGreeting(name: String): Unit \ { MyPrint, MyTime } = { let hour = MyTime.getCurrentHour(); if (hour < 12) MyPrint.println("Good morning, ${name}") @@ -194,18 +194,18 @@ def sendAll(l: List[Int32], tx: Sender[Int32]): Unit \ Chan = /// A function that receives n elements /// and collects them into a list. -def recvN(n: Int32, rx: Receiver[Int32]): List[Int32] \ {Chan, NonDet} = +def recvN(n: Int32, rx: Receiver[Int32]): List[Int32] \ { Chan, NonDet } = match n { case 0 => Nil case _ => Channel.recv(rx) :: recvN(n - 1, rx) } /// A function that calls receive and sends the result on d. -def wait(rx: Receiver[Int32], n: Int32, tx: Sender[List[Int32]]): Unit \ {Chan, NonDet} = +def wait(rx: Receiver[Int32], n: Int32, tx: Sender[List[Int32]]): Unit \ { Chan, NonDet } = Channel.send(recvN(n, rx), tx) /// Spawn a process for send and wait, and print the result. -def main(): Unit \ {Chan, NonDet, IO} = region rc { +def main(): Unit \ { Chan, NonDet, IO } = region rc { let l = 1 :: 2 :: 3 :: Nil; let (tx1, rx1) = Channel.buffered(100); let (tx2, rx2) = Channel.buffered(100); diff --git a/src/java-collections.md b/src/java-collections.md index e687c002..06794385 100644 --- a/src/java-collections.md +++ b/src/java-collections.md @@ -25,15 +25,15 @@ The following functions _convert_ Flix collections to Java collections: /// /// Lists /// -def toList(ma: m[a]): JList[a] \ IO + Aef[m] with Foldable[m] -def toArrayList(ma: m[a]): JArrayList[a] \ IO + Aef[m] with Foldable[m] -def toLinkedList(ma: m[a]): JLinkedList[a] \ IO + Aef[m] with Foldable[m] +def toList(ma: m[a]): JList[a] \ { Aef[m], IO } with Foldable[m] +def toArrayList(ma: m[a]): JArrayList[a] \ { Aef[m], IO } with Foldable[m] +def toLinkedList(ma: m[a]): JLinkedList[a] \ { Aef[m], IO } with Foldable[m] /// /// Sets /// -def toSet(ma: m[a]): JSet[a] \ IO + Aef[m] with Order[a], Foldable[m] -def toTreeSet(ma: m[a]): JTreeSet[a] \ IO + Aef[m] with Order[a], Foldable[m] +def toSet(ma: m[a]): JSet[a] \ { Aef[m], IO } with Order[a], Foldable[m] +def toTreeSet(ma: m[a]): JTreeSet[a] \ { Aef[m], IO } with Order[a], Foldable[m] /// /// Maps diff --git a/src/logger.md b/src/logger.md index b99aa46e..3117802c 100644 --- a/src/logger.md +++ b/src/logger.md @@ -61,7 +61,7 @@ mod Logger { The convenience functions accept any value that implements `Formattable`: ```flix -def main(): Unit \ { Logger } = +def main(): Unit \ Logger = Logger.info("Application started"); Logger.debug("Loading configuration..."); Logger.warn("Cache size exceeds threshold"); diff --git a/src/main.md b/src/main.md index 9ab745f5..cf93fd5f 100644 --- a/src/main.md +++ b/src/main.md @@ -25,7 +25,7 @@ For example, main can use the `Env` and `Exit` effects: use Sys.Env use Sys.Exit -def main(): Unit \ {Env, Exit} = +def main(): Unit \ { Env, Exit } = let args = Env.getArgs(); match List.head(args) { case None => @@ -44,7 +44,7 @@ The command line arguments passed to the program can be accessed by calling ```flix use Sys.Env -def main(): Unit \ {Env, IO} = +def main(): Unit \ { Env, IO } = let args = Env.getArgs(); println("Arguments: ${args}") ``` diff --git a/src/mutable-collections.md b/src/mutable-collections.md index 73406587..6c442d2d 100644 --- a/src/mutable-collections.md +++ b/src/mutable-collections.md @@ -59,7 +59,7 @@ def sweetFruits(rc: Region[r]): MutList[String, r] \ r = MutList.push("Pear") !> MutList.push("Mango") -def printFruits(fruits: MutList[String, r]): Unit \ {r, IO} = +def printFruits(fruits: MutList[String, r]): Unit \ r + IO = MutList.forEach(println, fruits) ``` diff --git a/src/polymorphic-effects.md b/src/polymorphic-effects.md index 939f5ea5..4ca59a5a 100644 --- a/src/polymorphic-effects.md +++ b/src/polymorphic-effects.md @@ -146,7 +146,7 @@ must be used with the same type arguments everywhere. For example, if we write: ```flix -def f(): Unit \ Emit[Int32] + Emit[String] = +def f(): Unit \ { Emit[Int32], Emit[String] } = Emit.emit(42); Emit.emit("Hello") ``` @@ -158,9 +158,9 @@ The Flix compiler emits the error message: >> Mismatched type arguments for effect 'Emit': 'Int32' and 'String'. -5 | def f(): Unit \ Emit[Int32] + Emit[String] = - ^^^^^^^^^^^^ - mismatched effect type argument. +5 | def f(): Unit \ { Emit[Int32], Emit[String] } = + ^^^^^^^^^^^^ + mismatched effect type argument. The effect 'Emit' is used with different types for its 1st type parameter 't'. diff --git a/src/test-framework.md b/src/test-framework.md index ec8e4617..41070ef6 100644 --- a/src/test-framework.md +++ b/src/test-framework.md @@ -98,7 +98,7 @@ A function marked with `@Test` must have one of the following signatures: @Test def test01(): Unit = ... def test02(): Unit \ Assert = ... -def test03(): Unit \ Assert + IO = ... +def test03(): Unit \ { Assert, IO } = ... ``` In addition, a `@Test` function may use any algebraic effect for which there is