--- title: "Design meeting 2024-12-18: Effects codelab" tags: ["T-lang", "design-meeting", "minutes"] date: 2024-12-18 discussion: https://rust-lang.zulipchat.com/#narrow/channel/410673-t-lang.2Fmeetings/topic/Lang.20team.20day.202024-12-18 url: https://hackmd.io/08xGAKpfS3KSkkLpC9FNvg --- # Effects codelab + discussion People: TC, Nikomatsakis, Josh, tmandry, scottmcm, Magnus Madsen, Matthew Lutze, Jonathan S, Eric Holk, Yosh ## Flix quickstart (10 min) You can either work in the [online playground](https://play.flix.dev/) or set up a Flix install locally. For more detailed documentation and alternatives, see [Getting Started](https://doc.flix.dev/getting-started.html) in the Flix book. 1. Install Java and VSCode (make sure java is in your PATH). Test it with `java -version`. 2. Open VSCode. 3. Install the Flix extension. 4. Open a new directory. 5. Add a file called `hello.flix` with the below contents. 6. Open the VSCode Terminal (it should open to the Flix REPL) and type `main()` to see the output. ### hello.flix ``` def main(): Unit \ IO = println("Hello World!") ``` ### Installing Java on macOS ``` brew install java sudo ln -sfn /opt/homebrew/opt/openjdk/libexec/openjdk.jdk /Library/Java/JavaVirtualMachines/openjdk.jdk ``` ## Getting familiar with Flix (10-15 min) Try going through some basic examples, like the one in the [Getting Started/Next Steps](https://doc.flix.dev/next-steps.html) (code in this section is outdated) section and on the [Home page](https://flix.dev/) to get familiar with the basic syntax and constructs. ## Codelab (45 min) Look over the Flix book chapter on its [effect system](https://doc.flix.dev/effect-system.html) again. Pick one of the Rust "effects": * async * gen * try * coro * panic * unsafe * const/runtime Model this effect in Flix. Add some test cases to demonstrate it working. It's a good idea to start with a simplified version and refine it over time. Once you are satisfied with the effect you chose, pick another. To start you off, perhaps, here are some Rust demos of our effects. For async, coroutines, and gen blocks, the examples are matched to show the same behavior, to highlight the similarities between these effects, and to show the way in which the desugaring of our `await` handler goes beyond the effect itself. Async: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2024&gist=a9f52c6fc6b7c72fc4f78103522c4b00 Coroutines: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2024&gist=5b7502185cdfc1b90aaa3cd101bdbcc8 Gen blocks: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2024&gist=5477624d5abe29dafc3bfb239f54806d Try blocks: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2024&gist=9118acbcdff7b6c0d962777d419e3b6f ## Show and tell ### Niko nikomatsakis: [try effect](https://play.flix.dev/?q=FAEwpgZgBAtghgSwHYAoCUAuKBVJCAuUAOlAJIDyUAvFMFPVAE4CuSUA3nQ9wDZiE9kYAM7UorAO6M4ABxQg4+OOjQBuLt3p9C+APZKeAGSGiax4fgB0fJAHN8ACxSCkItRs3aoegwHVdjCCmUOZWwswwvgROSBEARmCM5BD+gcIANFAubuqaDDKMyPg8qABExq7CWAAk7D5wRiYAvpmpQTV1+g1twk2laB5NUBLRUAAqjACeHB7c4NAQiDwoAPpoYgVFJSiliYwB-YMewJDQE9Occ5BQiwjLmFAAaroIIMBNwKDXktJyjFgAJREzB4+AA2mBMvgALoPQgkc7UDzwfAAYwcTBmeXoqLgwjAUHIAGsUAA3dZUAB8UFJswYuPxUAAooxGKsKdTzpZbvcjl9oLEYAkkikAkEUFUoABlfCFOwPUhIfAAZgATGIPKIAD7UmVy2yWCRi7XU0LWMB2RzHeZQBRKdCA4GgsG4AiZUJgvXIWzQ6FI7jElChADE7FK+BE+FKmXDkdKTTQQA) ```flix def main(): Unit \ IO = run { let lines = unwrap(data()); let totalLines = List.length(lines); let totalWords = List.sumWith(numberOfWords, lines); println("Lines: ${totalLines}, Words: ${totalWords}") } with Try { def fail(_) = println("error") } eff Try { def fail(): Void } def unwrap(r: Result[e, t]): t \ Try = match r { case Ok(v) => v case Err(_) => Try.fail() } def numberOfWords(s: String): Int32 = s |> String.words |> List.length def data(): Result[Unit, List[String]] = Ok(List#{"test", "test"}) ``` nikomatsakis: [unsafe effect](https://play.flix.dev/?q=KYMxAIFUDsGcEMTHAbwFDk+AJqc9ZZgAnAFwAoBjAe2mwEtT7aAucAZVOPugHMBKNjEZoAvmjS4IAB2DAA1uXpsAktFIBmAEyDwazVvAAdKHETIAvBiwwESAHQEiZcgCIAbvAA29bOQAkKPSi-K78ANzWmFpaElLgxMDw2AD62PCk8OS6+trgVlgJAK7QqFGF4LIK5AAM-OWi4ADujAAWpnbI6BVY8U4kFO4ANODy-Png9BDk7vkW4B7evrWh44rZ48BeROAAtACMDXF4ALbwPNmq6nkFWInJaRlZ-EA) ```flix eff Unsafe { def assert(): Unit } def peek(i: Int32): Int32 \ Unsafe = Unsafe.assert("valid(${i})"); 22 def read_data(): Int32 = run { peek(0) } with Unsafe { def assert(_, k) = k(()) } def main(): Int32 = read_data() ``` ```flix eff Unsafe { def assert(condition: String): Unit } def peek(i: Int32): Int32 \ Unsafe = Unsafe.assert("valid(${i})"); 22 def read_data(): Int32 = run { peek(0) } with Unsafe { def assert(v, k) = if (v == "valid(0)") k(()) else -1 } def main(): Int32 = read_data() ``` could also have a "family" of unsafe effects ```flix eff UnsafeDeref { def assert(address: Int32): Unit } def peek(i: Int32): Int32 \ UnsafeDeref = UnsafeDeref.assert(i); 22 def read_data(): Int32 = run { peek(0) } with UnsafeDeref { def assert(_addr, k) = k(()) } def main(): Int32 = read_data() ``` nikomatsakis: you can kind of think of safe code as inserting `run { } with Unsafe { }` blocks (e.g., borrow checker) for the pointers we introduce. you can also imagine building up a safe abstraction. joshtriplett: My experiments mostly tried effects that acted as callbacks, e.g. `Gen.item` processed the item and resumed. I could imagine instead having capability effects that return values, like an FS effect that has an open operation, and then you can `run-with` a real FS or a mock FS. nikomatsakis: yeah I considered this but I rejected it because it didn't accommodate the idea of "C code" -- i.e., things where the deref is not going through an effect handler but is "built-in" ```flix eff UnsafeDeref { def peek(i: Int32): Int32 } def read_c_string(i: Int32): Int32 \ UnsafeDeref = while UnsafeDeref.peek(i) != 0 { i += 1 } ``` ```flix eff UnsafeDeref { def assert(address: Int32): Unit } // equivalent of `*x` def peek(i: Int32): Int32 \ UnsafeDeref = UnsafeDeref.assert(i); 22 // equivalent of `unsafe fn peek_doubled()` def peek_doubled(i: Int32): Int32 \ UnsafeDeref = peek_doubled(i) // equivalent of `fn read_doubled() { unsafe { peek_doubled(0) } }` def read_doubled_data(): Int32 = run { peek_doubled(0) } with UnsafeDeref { def assert(_addr, k) = k(()) // ^^^^^ doing anything else here is not a faithful representation } def main(): Int32 = read_data() ``` TC: Looking at the above, what we really want to do is to do that assert at compile time, of course. Perhaps if we were to combine our notion of const-eval with effects, we get a compile-time notion of effects and handlers, and then we directly model `unsafe` in that way, where the handler may panic at compile-time. ### Tyler tmandry: [async effect](https://play.flix.dev/?q=KYMxAIEEGcE8DsDG4DeAocnwBNTmgK7QAOw82AFAJQBc4AqvAJYAuGWuEJAhgO7wwEiCiDqNW4ALQA+BsxbgAOlDhJac1mgC+aNJ3AAnYN2ywKADzot1C5YKTgAvO0xMII7gBtowKqhVCAHSEJGSUVADchsamFn5a4MDewP7m4Dp6eKTkTPAA5tR03PCwSgEOjqgu5YjBRNnhEdUNuQVU2rr6uJ7cZgD6LEwAtsB0AJLwLADMAEzq4rY1TlVY4AD0a+AACkYsYeC8Kd294AAWwEaB1UYmZtTtGfpDsH3cqoh9IPCFGoso9ogADTgMYAeQSlXQq2OZgAjAAGeGRZoGXIsTzfABEAAkkp4APYHfEGTzYACEmOR0KSvQoCKRHUyEAMBHgfVyfWA5mAwlEvyksgWZQoeAA1DUqPN5GU8JUjHkmPj4IZkFCsJ5gAoWG8ANbQZYAWQILAAMkxoCxAsAhsQWGYDIgqVgjabzZbiERTiJgdroHqnZgWWyuTyKL7-YyNlBPEN8RaiQYda1AimmYZWZzucJw9A6C6zRaANpCmT85QiiDigFUAC6UokyllKywQ24LEQp3A+bdgWI+OIYd10D8atWmEQbxSAGV8SNB36-I5ZEH-OHqOkDqxOwDm2Ox-oQg0KH1gUZCCNF7u99fuxbe56KGeCCMfUOA9e90HM6Gc+0P6sdH-Dg8B4fgAXnHVT2Ac9fGWUcgOdY0C3dB9w1fP1hyaBCx1vFDoC9J8X3AX8sOwrAvxDbM32qD9AOvOi9wnHxwAAOSVFIl3Aahqh0R48AAK3xXIKG4MRpVLRYK3AKt3ioYEACMxIkCThTFCV1GQwsWBrVTKwlZZ5UVZUHSvDUFBafJlm4cAaDoeSbLoFimE8UjMDM6JbkNJCe2tW17UdVz02VeDVhAYljA7LiIAAHkkcALLyEcaJvby7w9fCRGoKDbj-ejAtWXDAhYfFkMfGJYFyzAEl4LclhC4CuHqMJjygmDL3+d46lCchqCiQjgGoBj9xA4g+AEd5vWiNq4IBYJRrAiaQEiKbnwGqght4tAgA) ```flix eff Async { def suspend(): Unit def spawnAsync(f: Unit -> Unit \ Async): Unit } def ready(x: t): t \ Async = if (false) { Async.suspend(); ready(x) } else { x } def pending(): any \ Async = { Async.suspend(); pending() } def delay(_time: Int32): Unit \ Async = { // Pretend we delay here. ready(()) } def my_async_fn(): Unit \ {Async, IO} = { delay(100); println("Hello world!"); delay(100) } def run_in_exec(f: Unit -> Unit \ (ef + Async)): Unit \ ef = region rc { let tasks = MutList.empty(rc); MutList.push(f, tasks); run_exec(tasks) } // Almost working... def run_exec(tasks: MutList[Unit -> Unit \ (ef + Async)]): Unit \ ef = { match MutList.pop(tasks) { case Some(task) => run { task() } with Async { def suspend(_, resume) = { MutList.push(resume, tasks); run_exec(tasks) } def spawnAsync(task, resume) = { MutList.push(task, tasks); MutList.push(resume, tasks); run_exec(tasks) } } case None => () } } def join(a: Unit -> t \ (ef + Async), b: Unit -> t \ (ef + Async)): List[t] \ (ef + Async) = region rc { let pending = a :: b :: Nil; let ready = MutList.empty(rc); run { foreach (f <- pending) { MutList.push(f(), ready) }; MutList.toList(ready) } with Async { def suspend(_, resume) = {Async.suspend(); resume()} def spawnAsync(f, resume) = {Async.spawnAsync(f); resume()} } } ``` ```flix eff Async { def suspend(f: ): Unit def spawnAsync(f: Unit -> Unit \ Async): Unit } // Almost working... def run_exec(tasks: MutList[Unit -> Unit \ (ef + Async)]): Unit \ ef = { match MutList.pop(tasks) { case Some(task) => run { task() } with Async { def suspend(waitable, resume) = { waitable.wake_when_ready(|| MutList.push(resume)) // ----------------------- this is the Rust `Waker`, more or less // in Rust, this takes place "in the future" but you don't have the continuation there // so I wanted to do it here run_exec(tasks) } def spawnAsync(task, resume) = { MutList.push(task, tasks); MutList.push(resume, tasks); run_exec(tasks) } } case None => () } } def leaf_future(reactor): Int32 \ Async { loop { Async.suspend(|waker| { reactor.register_waker(waker) }); 33 } } ``` ### Josh - [Gen effect](https://play.flix.dev/?q=KYMxAIHFgO3BvAUOF4AmpwEsAuwC2AFFgFzgCSMOAzAEwCUZAqjLogL6KIYT4CGWGIUbgWucAB0KAeXABecMlQAnAK5wkqLegD20IfSUp24AO64AFlFgIj2ntjxEsAGnDLgAZ1X5g9ebbaQeAADsqCOAA2QgBEkDo44AAk8FjsMfQA3HbB7l4+wMI5qJxapajcmGh6sMLMrIlS+gGaqPoAdLgEhACMtNRZdh1dRAAsAKwAbINaw06EAOwAHACchuxAA) - [Gen effect with infinite generator and a handler that doesn't always resume](https://play.flix.dev/?q=KYMxAIHFgO3BvAUOF4AmpwEsAuwC2AFFgFzgCSMOAzAEwCUZAqjLogL6KIYT4CGWGIUbgWucAB0KAeXABecMlQAnAK5wkqLegD20GADEdy4ADdgywgAZ6SlO3AB3XAAsosBHe09seIlgAacBMAZ1V8YHp5T21Y8AAHZUEcABshACJIHRxwABJ4LHZ0+gBuLzjsCGJ5BQBGGxiKuMTktMJ0gDlwgCMLbBDweqCYbODgMPxBAHNi8riHYBSQ4Eam7VDw4GE52M4KvdQDlG5MND1YIxNzS1IKKjoRMRypfWjNVH0AOlwCYlKvM76S5mCzVADUg1s7CAA) - [Panic effect](https://play.flix.dev/?q=KYMxAIAUEMDsEsDG4DeAocnwBNTgA5xIAUAzgFzgDKALgE7ywDmAlJQGoD282aAvmjS4IAW2iNibcAFUENcAB1wASQDy4ALzgMWOgFdYqHViz4GsGgBtYxAESka0OjVssA3MZPgR8JgAsaGAREYgAWACZ3TxMzRisbW1hgAA8XKK8sH39AohCAVnSMgnN4u0toGmA6Vw8i718AoJIAZkKM2ItrO2xOJNdPPnAAd3gaPyhcozrhAlyyABpwAH06YFI9EWAWTSm60xKu20gAQQA5ZQBhSgASFFI+frqBE2esITwsxrn4SmULZsilFko0UE2CO3QJngEGI8E0WgKuwyTUQADpCME7AAxZTsACiAH4AISPLCDYCWUjAJFeSQDfhAA) Gen effect: ```flix eff Gen { def item(i: Int32): Unit } def main(): Unit \ IO = run { doGen() } with Gen { def item(i, resume) = { println("Got ${i}"); resume() } } def doGen(): Unit \ Gen = { Gen.item(123); Gen.item(456); Gen.item(789) } ``` Gen effect with infinite generator: ```flix eff Gen { def item(i: Int32): Unit } def main(): Unit \ IO = run { doGenForever(0) } with Gen { def item(i, resume) = { println("Got ${i}"); if (i == 10) { println("Number is 10, not resuming") } else { resume() } } } def doGenForever(i: Int32): Unit \ Gen = { Gen.item(i); doGenForever(i + 1) } ``` nikomatsakis: just for fun, map combinator ```rust eff Gen { def item(i: Int32): Unit } def main(): Unit \ IO = run { doGenDoubled() } with Gen { def item(i, resume) = { println("Got ${i}"); resume() } } def doGen(): Unit \ Gen = { Gen.item(1); Gen.item(2); Gen.item(3) } def doGenDoubled(): Unit \ Gen = { run { doGen() } with Gen { def item(i, resume) = { Gen.item(i * 2); resume() } } } ``` Panic effect: ```flix! eff Panic { def panic(s: String): Void } def main(): Unit \ IO = run { println("start"); mightPanic(42); println("next"); mightPanic(5); println("later"); mightPanic(3); println("done") } with Panic { def panic(s, _resume) = { println("PANIC: ${s}") } } def mightPanic(i: Int32): Unit \ Panic = { if (i == 5) { Panic.panic("FIVE?!") } else { () } } ``` #### Difference between effects and generic arguments Magnus: [Effekt](https://effekt-lang.org/) uses capabilities to support handlers. Flix is a more traditional effect system. One of the things that this makes easier is set operations with effects. One example of something hard to model without sets is forbidden effects: This callback can do anything *except* commit to the database. Matt: Discussion of _effect exclusion_ here: https://dl.acm.org/doi/abs/10.1145/3607846 ### TC To Josh's point about `for` loops being the `gen` effect kind of inverted, see also e.g. the Koka `gen` example from RFC 3513: ```koka effect yield<a> fun yield(x : a) : () fun odd_dup(xs : list<int>) : yield<int> () match xs Cons(x,xx) -> if x % 2 == 1 then yield(x * 2) odd_dup(xx) Nil -> () fun main() : console () with fun yield(i : int) println(i.show) list(1,20).odd_dup ``` With apologies to the Flix folks, I took the time today to better understand the `effing-mad` crate and model the `gen` effect, with an `async`-like `block_on` in `effing-mad`. Note that `effing-mad` does support generic effects. ```rust #![feature(coroutines)] #![feature(coroutine_trait)] use core::{ sync::atomic::{AtomicU64, Ordering}, task::Poll, }; use effing_mad::{effectful, handle_group, handler, run}; static COUNT: AtomicU64 = AtomicU64::new(0); effing_mad::effects! { Gen<T> { fn yld(x: T) -> (); } } #[effectful(Gen<Poll<T>>)] fn pending_then<T: Copy>(x: T) { COUNT.fetch_add(1, Ordering::Relaxed); yield Gen::yld(Poll::Pending); COUNT.fetch_add(1, Ordering::Relaxed); yield Gen::yld(Poll::Pending); COUNT.fetch_add(1, Ordering::Relaxed); yield Gen::yld(Poll::Pending); yield Gen::yld(Poll::Ready(x)); } fn main() { let block_on = |f| { let mut output = None; let h = handler!(Gen<Poll<u64>> { yld(x) => { match x { Poll::Pending => (), Poll::Ready(x) => output = Some(x), }; }, }); let handled = handle_group(f, h); run(handled); output.unwrap() }; let x = block_on(pending_then(3u64)); let y = block_on(pending_then(5u64)); let z = x + y; assert_eq!(8, z); assert_eq!(6, COUNT.load(Ordering::Relaxed)); } ``` ### Jonathan ```flix eff Unsafe {} def unsafeDeref(x: pointer[t]): t \ Unsafe = ??? def runInSafeContext(f: Unit -> Unit \ ef - Unsafe): Unit \ ef - Unsafe = f() def unsafeBlock(f: Unit -> Unit \ ef + Unsafe): Unit \ ef = unsafely Unsafe run f def randomFun(x: pointer[Int32], y: pointer[Int32]): Int32 \ Unsafe = unsafeDeref(x) + unsafeDeref(y) def entry(): Unit = runInSafeContext(() -> println(randomFun(???, ???))) // Expected: Unit -> Unit \ (Unsafe & e0) - Unsafe // Actual: Unit -> Unit \ e0 + IO ``` ```flix mod Examples { use Gen.yld use Try.crash use My.IteratorImpl pub eff Gen { // `yield` is a reserved keyword def yld(x: Int32): Unit } pub eff Try { def crash!(msg: String): Void } pub eff SoftTry { def crash!(msg: String): Unit } pub def preciseDivByN(n: Int32, l: List[Int32]): Unit \ Gen + Try = { foreach (i <- l) { if (Int32.remainder(i, n) == 0) yld(i / n) else crash("${i} is not precisely divisible by ${n}") } } def _sandbox(): Unit \ State = { let l = List#{2, 4, 7}; // an iterator of crashing functions let it1: IteratorImpl[Int32, State + Try] = run preciseDivByN(2, l) with fun Gen.runIteratorImpl; let _it2 = it1 |> IteratorImpl.wrap(Try.runOpt); // an iterator that gives up when it crashes let _it3 = run preciseDivByN(2, l) with fun Try.runDefault(()) with fun Gen.runIteratorImpl; let _it4: Unit -> Result[String, Unit] \ Gen = () -> run preciseDivByN(2, l) with fun Try.runRes; () } } ``` Josh: In adding a way to have iterators that can fail permanently, we should attempt to make sure that both "iterator that can fail permanently" and "iterator of things that can individually fail" are both cleanly represented in the type system (and desugar to types), rather than one of them using a system that the other can't. TC: We can smoothly express both, e.g.: ```rust! try gen fn f() yield u8 yeet String {} // -> impl TryGen<..> { do try gen { ... } } gen fn f() yield impl Try<u8, String> {} // -> impl Gen<impl Try<..>>{ do gen { ... } } ``` NM: Yeah more or less this. Josh: This is what I'd consider second class: the fact that combining `try` and `gen` requires a special `TryGen` rather than being able to be combined generically, while the other order can be combined generically. (And in this particular example, that `try gen fn` is a thing but the other thing has to be written `gen fn() ... impl Try`) NM: This doesn't seem second class to me. ## Discussion ### Destructors TC: One of the challenges we face in encoding `const` as an effect in Rust is the handling of destructors in a generic context. Does Flix have to think about that? ```rust fn f<T: Fn() /* + ~const Destruct ?? */>() {} ``` Jonathan: It can be difficult to model these with (multi-shot) algebraic effects. I think Effekt had a paper on it. You can end up at the end of your scope between 0 and infinite times with multi-shot algebraic effects. NM: Related to [must move types](https://smallcultfollowing.com/babysteps/blog/2023/03/16/must-move-types/). I don't personally see why this is hard but it will likely lead to painful composition, I guess that's the problem? Jonathan: Here is the paper from the Effekt people about combining effects and destructors https://se.cs.uni-tuebingen.de/publications/schuster22region.pdf. Its a bit on the theoretic side. It does not consider the effects of the destructors as far as i can see (like what is the first destructor fails). ### Negative effect bounds Yosh: How would we model the equivalent to `!const fn` (e.g. bare `fn`) in Flix? We need this to gradually move types and traits over to support effects. In today's Rust we can write this today: ```rust struct MyType; impl MyType { // this function is callable from `const` contexts const fn const_available(&self) { .. } // this function is not callable from `const` contexts fn const_unavailable(&self) { .. } } ``` In the Rust stdlib we make use of this to gradually mark more `const` items as `#[const_stable(since=..)]`, providing a gradual roll-out of const APIs. We can imagine wanting to do the same for traits: ```rust pub trait Meow { // this function is callable from `const` contexts const fn const_available(&self) { .. } // this function is not callable from `const` contexts fn const_unavailable(&self) { .. } } ``` Now, let's try translating this to a system using associated effects. We'd need some way to say: "this method is not available if the associated effect includes `const`" - how would we do that? ```rust pub trait Meow { // associated effect on the trait // - assume this may carry any effect including `const` effect Eff = {}; // This function is available for any effects // in `Self::Eff` fn const_available(&self) \ Self::Eff { .. } // This function is not available if `Self::Eff` // carries a `const` effect fn const_unavailable(&self) where Self::Eff: { !const } // <- Can we do something like this? } ``` Matt: In Flix you might do something like this: ```flix def const_unavailable(x: a) \ Meow.Eff[a] with Meow[a] where Meow.Eff[a] ~ (Meow.Eff[a] - Const) // equiv. to `with Meow[a] where Const ∉ Meow.Eff[a]` ``` NM: I feel like the runtime effect makes this very clear. ```flix eff Runtime { } // const fn must_be_const(f: const Fn()) // or something def must_be_const(f: Fn() \ eff1 - Runtime) def could_be_anything(f: Fn()) ``` ### cramertj scratchpad #### Why is [`Iterator` a single-case enum?](https://github.com/flix/flix/blob/master/main/src/library/Iterator.flix#L17-L19) I suspect there's a pattern here I don't understand. Possibly this is similar to Rust's wrapper type pattern? (e.g. `struct Wrapper<T>(T);` to allow for adding trait impls). Jonathan: i little demo of three different ways to express an iterator (simplified to not use regions). Rust iterator is more like the trait version, but since we cant return an Iterator trait impl, the concrete iterator is more useful. ```flix // An iterator is defined by its `next` function. // .. either directly pub type alias IteratorFun[t: Type, ef: Eff] = Unit -> Option[t] \ ef // .. or expressed through a trait pub trait Iterator[t] { type Item: Type type Eff: Eff pub def next(x: t): Option[Iterator.Item[t]] \ Iterator.Eff[t] } // .. or packed in a data structure pub enum IteratorImpl[t: Type, ef: Eff](Unit -> Option[t] \ ef) ``` #### Monad isn't effect-based It's interesting that [the `Monad` trait](https://github.com/flix/flix/blob/master/main/src/library/Monad.flix#L25-L28) bubbles its effect to the top-level rather than operating on input values that are already effect-ed. Since monads are often used to model many effect-like things, this trait signature seems like it has two separate ways to model effects. Notably, [the `yield` documentation](https://doc.flix.dev/monadic-for-yield.html#monadic-for-yield) says that it uses `do`-notation-style sugar based on `point` and `flatMap` rather than an effect. This was surprising to me! Matt: There are a couple reasons for our monad support. First is just historical: we had monads before we had effects. Second is that it's still a useful abstraction, even when you have effects. (Containers, etc.)