Algebraic Effects System for Rust

Vic/jj change lznvqqpxrpll (#18)

* refactor(abilities_macro): remove boxes and generalize ability construction 🧩

- Updated macro to return impl Ability instead of Box for ability construction functions.
- Generalized method signatures to allow any Ability type, not just boxed trait objects.
- Updated integration tests to match new API and type signatures.
- Improves ergonomics and flexibility for effectful code and ability usage.

Future work:
- Extend macro to support more context shapes and derive HasPut for structs.
- Refactor Pair usage in effect APIs for full genericity.

* feat(core): add provide_has for ergonomic partial context provision 🧩

- Added Fx::provide_has, allowing partial context provision using HasPut/Put trait, supporting both tuples and structs.
- Added tests for provide_has with struct context (i32 and String fields).
- Improves ergonomics for effectful code with custom context shapes.

Future work:
- Extend HasPut/Put derive macros for easier struct support.
- Refactor provide_left and related APIs to use HasPut for full genericity.

* refactor(core): rename provide_has to update_context and improve context mutation API 🧩

- Renamed Fx::provide_has to Fx::update_context for clarity and ergonomics.
- Updated all tests and usages to use update_context.
- Added and improved tests for struct-based context mutation and handler replacement.
- This change clarifies the API semantics, making context adaptation and mutation explicit and more general.

Future work:
- Extend HasPut/Put derive macros for easier struct support.
- Refactor related APIs for full genericity and ergonomic context adaptation.

authored by oeiuwq.com and committed by

GitHub 80f311bd 3cc93e77

+83 -12
+5 -5
crates/abilities_macro/src/lib.rs
··· 27 27 // Associated function for ability construction 28 28 output.extend(quote! { 29 29 impl #trait_ident { 30 - pub fn #ability_fn_ident<'f, F>(f: F) -> Box<dyn Ability<'f, #input_ty, #ctx_ty, #ret_ty> + 'f> 30 + pub fn #ability_fn_ident<'f, F>(f: F) -> impl Ability<'f, #input_ty, #ctx_ty, #ret_ty> + 'f 31 31 where F: FnOnce(#input_ty) -> Fx<'f, #ctx_ty, #ret_ty> + Clone + 'f { 32 - Box::new(f) 32 + f 33 33 } 34 - pub fn #method_ident<'f, P>(arg: #input_ty) -> Fx<'f, P, #ret_ty> 35 - where P: Pair<Box<dyn Ability<'f, #input_ty, #ctx_ty, #ret_ty> + 'f>, #ctx_ty> { 36 - Abilities::request::<P, Box<dyn Ability<'f, #input_ty, #ctx_ty, #ret_ty> + 'f>>(arg) 34 + pub fn #method_ident<'f, P, A>(arg: #input_ty) -> Fx<'f, P, #ret_ty> 35 + where A: Ability<'f, #input_ty, #ctx_ty, #ret_ty> + 'f + Clone, P: Pair<A, #ctx_ty> { 36 + Abilities::request::<P, A>(arg) 37 37 } 38 38 } 39 39 });
+10 -1
crates/fx/src/core/provide.rs
··· 1 - use crate::{core::pair::Pair, kernel::fx::Fx}; 1 + use super::super::kernel::fx::Fx; 2 + use super::{has_put::Put, pair::Pair}; 2 3 3 4 impl<'a, S: Clone, V: Clone> Fx<'a, S, V> { 4 5 pub fn provide<T: Clone>(self, s: S) -> Fx<'a, T, V> { ··· 13 14 B: Clone + 'a, 14 15 { 15 16 self.contra_map(|b: B| cmap(a, b), fmap) 17 + } 18 + 19 + pub fn update_context<T>(self, t: T) -> Fx<'a, S, V> 20 + where 21 + S: Put<T>, 22 + T: Clone + 'a, 23 + { 24 + self.contra_map(|p: S| p.put(t), |_, p| p) 16 25 } 17 26 } 18 27
+62 -1
crates/fx/src/core/tests/provide_test.rs
··· 1 - use crate::kernel::fx::Fx; 1 + use crate::{Has, Put, kernel::fx::Fx}; 2 2 3 3 #[test] 4 4 fn provide_sets_state() { ··· 13 13 let fx2 = fx.provide_left::<i32, i32>(1); 14 14 let _ = fx2; 15 15 } 16 + 17 + #[derive(Clone, Debug, PartialEq)] 18 + struct Ctx { 19 + a: i32, 20 + b: String, 21 + } 22 + 23 + impl Has<i32> for Ctx { 24 + fn get(self) -> i32 { 25 + self.a 26 + } 27 + } 28 + impl Put<i32> for Ctx { 29 + fn put(mut self, value: i32) -> Self { 30 + self.a = value; 31 + self 32 + } 33 + } 34 + 35 + impl Put<String> for Ctx { 36 + fn put(mut self, value: String) -> Self { 37 + self.b = value; 38 + self 39 + } 40 + } 41 + 42 + #[test] 43 + fn provide_has_sets_field() { 44 + let ctx = Ctx { 45 + a: 0, 46 + b: "init".to_owned(), 47 + }; 48 + let fx: Fx<Ctx, i32> = Fx::pending(|c: Ctx| Fx::value(c.a)); 49 + let fx2 = fx.update_context(42); 50 + assert_eq!(fx2.provide(ctx.clone()).eval(), 42); 51 + } 52 + 53 + #[test] 54 + fn provide_has_sets_string_field() { 55 + let ctx = Ctx { 56 + a: 7, 57 + b: "init".to_owned(), 58 + }; 59 + let fx: Fx<Ctx, String> = Fx::pending(|c: Ctx| Fx::value(c.b.clone())); 60 + let fx2 = fx.update_context("hello".to_owned()); 61 + assert_eq!(fx2.provide(ctx).eval(), "hello"); 62 + } 63 + 64 + #[test] 65 + fn update_context_replaces_part_of_context() { 66 + let ctx = Ctx { 67 + a: 7, 68 + b: "init".to_owned(), 69 + }; 70 + let a = Fx::func(|u: i32| u * 2).lift(); 71 + let fx = a 72 + .clone() 73 + .map_m(|n| a.update_context(10i32).map(move |m| (n, m))); 74 + let v = fx.provide(ctx).eval(); 75 + assert_eq!(v, (14, 20)) 76 + }
+6 -5
crates/integration/tests/abilities_macro_test.rs
··· 27 27 #[test] 28 28 fn ability_from_closure() { 29 29 let _: Box<dyn Ability<'_, String, (), usize>> = 30 - Foo::length_ability(|line: String| Fx::value(line.len())); 30 + Box::new(Foo::length_ability(|line: String| Fx::value(line.len()))); 31 31 let _: Box<dyn Ability<'_, String, (), usize>> = Box::new(|line: String| Fx::value(line.len())); 32 32 } 33 33 ··· 39 39 { 40 40 } 41 41 check_type(Abilities::request::<(_, _), _>("hello".to_owned())); 42 - check_type(Foo::length::<(_, _)>("Hello".to_owned())); 42 + check_type(Foo::length::<(_, _), _>("Hello".to_owned())); 43 43 } 44 44 45 45 #[test] 46 46 fn ctx_ability_from_closure() { 47 - let _: Box<dyn Ability<'_, u8, String, usize>> = 48 - Baz::len_add_ability(|n: u8| Fx::func(move |s: String| s.len() + (n as usize))); 47 + let _: Box<dyn Ability<'_, u8, String, usize>> = Box::new(Baz::len_add_ability(|n: u8| { 48 + Fx::func(move |s: String| s.len() + (n as usize)) 49 + })); 49 50 let _: Box<dyn Ability<'_, u8, String, usize>> = 50 51 Box::new(|n: u8| Fx::func(move |s: String| s.len() + (n as usize))); 51 52 } ··· 58 59 { 59 60 } 60 61 check_type(Abilities::request::<(_, _), _>(10u8)); 61 - check_type(Baz::len_add::<(_, _)>(10u8)); 62 + check_type(Baz::len_add::<(_, _), _>(10u8)); 62 63 }