diff --git a/doc/src/SUMMARY.md b/doc/src/SUMMARY.md index 3592dbc5..78ea9882 100644 --- a/doc/src/SUMMARY.md +++ b/doc/src/SUMMARY.md @@ -79,6 +79,7 @@ The Rhai Scripting Language 4. [Function Pointers](language/fn-ptr.md) 5. [Anonymous Functions](language/fn-anon.md) 6. [Currying](language/fn-curry.md) + 7. [Capturing External Variables](language/fn-closure.md) 16. [Print and Debug](language/print-debug.md) 17. [Modules](language/modules/index.md) 1. [Export Variables, Functions and Sub-Modules](language/modules/export.md) diff --git a/doc/src/engine/dsl.md b/doc/src/engine/dsl.md index 5b381d4a..8f326ed1 100644 --- a/doc/src/engine/dsl.md +++ b/doc/src/engine/dsl.md @@ -15,6 +15,17 @@ The [`Engine::eval_expression_XXX`][`eval_expression`] API can be used to restri a script to expressions only. +Unicode Standard Annex #31 Identifiers +------------------------------------- + +Variable names and other identifiers do not necessarily need to be ASCII-only. + +The [`unicode-xid-ident`] feature, when turned on, causes Rhai to allow variable names and identifiers +that follow [Unicode Standard Annex #31](http://www.unicode.org/reports/tr31/). + +This is sometimes useful in a non-English DSL. + + Disable Keywords and/or Operators -------------------------------- diff --git a/doc/src/language/fn-anon.md b/doc/src/language/fn-anon.md index 21ceb7a3..a4cb0fee 100644 --- a/doc/src/language/fn-anon.md +++ b/doc/src/language/fn-anon.md @@ -49,9 +49,13 @@ fn anon_fn_1001(x) { this.data -= x; } fn anon_fn_1002() { print this.data; } ``` + WARNING - NOT Closures ---------------------- Remember: anonymous functions, though having the same syntax as Rust _closures_, are themselves **not** closures. In particular, they do not capture their running environment. They are more like Rust's function pointers. + +They do, however, _capture_ variable _values_ from their execution environment, unless the [`no_capture`] +feature is turned on. This is accomplished via [automatic currying][capture]. diff --git a/doc/src/language/fn-closure.md b/doc/src/language/fn-closure.md new file mode 100644 index 00000000..d8749601 --- /dev/null +++ b/doc/src/language/fn-closure.md @@ -0,0 +1,54 @@ +Capture External Variables via Automatic Currying +================================================ + +Poor Man's Closures +------------------- + +Since [anonymous functions] de-sugar to standard function definitions, they retain all the behaviors of +Rhai functions, including being _pure_, having no access to external variables. + +The anonymous function syntax, however, automatically _captures_ variables that are not defined within +the current scope, but are defined in the external scope - i.e. the scope where the anonymous function +is created. + +Variables that are accessible during the time the [anonymous function] is created can be captured, +as long as they are not shadowed by local variables defined within the function's scope. +The values captured are the values of those variables at the time of the [anonymous function]'s creation. + + +New Parameters For Captured Variables +------------------------------------ + +In actual implementation, this de-sugars to: + +1. Keeping track of what variables are accessed inside the anonymous function, + +2. If a variable is not defined within the anonymous function's scope, it is looked up _outside_ the function and in the current execution scope - where the anonymous function is created. + +3. The variable is added to the parameters list of the anonymous function, at the front. + +4. The current value of the variable is then [curried][currying] into the [function pointer] itself, essentially carrying that value and inserting it into future calls of the function. + +Automatic currying can be turned off via the [`no_capture`] feature. + + +Examples +-------- + +```rust +let x = 40; + +let f = |y| x + y; // current value of variable 'x' is auto-curried + // the value 40 is curried into 'f' + +x = 1; // 'x' can be changed but the curried value is not + +f.call(2) == 42; // the value of 'x' is still 40 + +// The above de-sugars into this: +fn anon$1001(x, y) { x + y } // parameter 'x' is inserted + +let f = Fn("anon$1001").curry(x); // current value of 'x' is curried + +f.call(2) == 42; +``` diff --git a/doc/src/language/fn-curry.md b/doc/src/language/fn-curry.md index 8ee103a9..71c8933a 100644 --- a/doc/src/language/fn-curry.md +++ b/doc/src/language/fn-curry.md @@ -28,3 +28,12 @@ let curried = curry(func, 21); // function-call style also works curried.call(2) == 42; // <- de-sugars to 'func.call(21, 2)' // only one argument is now required ``` + + +Automatic Currying +------------------ + +[Anonymous functions] defined via a closure syntax _capture_ external variables that are not shadowed inside +the function's scope. + +This is accomplished via [automatic currying]. diff --git a/doc/src/language/oop.md b/doc/src/language/oop.md index e34a8ab2..09bc7b55 100644 --- a/doc/src/language/oop.md +++ b/doc/src/language/oop.md @@ -21,6 +21,18 @@ When a property of an [object map] is called like a method function, and if it h a valid [function pointer] (perhaps defined via an [anonymous function]), then the call will be dispatched to the actual function with `this` binding to the [object map] itself. + +Use Anonymous Functions to Define Methods +---------------------------------------- + +[Anonymous functions] defined as values for [object map] properties take on a syntactic shape +that resembles very closely that of class methods in an OOP language. + +Anonymous functions can also _capture_ variables from the defining environment, which is a very +common OOP pattern. Capturing is accomplished via a feature called _[automatic currying]_ and +can be turned off via the [`no_capture`] feature. + + Examples -------- diff --git a/doc/src/links.md b/doc/src/links.md index 936c714d..19fdf0e9 100644 --- a/doc/src/links.md +++ b/doc/src/links.md @@ -9,6 +9,7 @@ [`no_object`]: {{rootUrl}}/start/features.md [`no_function`]: {{rootUrl}}/start/features.md [`no_module`]: {{rootUrl}}/start/features.md +[`no_capture`]: {{rootUrl}}/start/features.md [`no_std`]: {{rootUrl}}/start/features.md [`no-std`]: {{rootUrl}}/start/features.md [`internals`]: {{rootUrl}}/start/features.md @@ -78,6 +79,8 @@ [function pointer]: {{rootUrl}}/language/fn-ptr.md [function pointers]: {{rootUrl}}/language/fn-ptr.md [currying]: {{rootUrl}}/language/fn-curry.md +[capture]: {{rootUrl}}/language/fn-closure.md +[automatic currying]: {{rootUrl}}/language/fn-closure.md [function namespace]: {{rootUrl}}/language/fn-namespaces.md [function namespaces]: {{rootUrl}}/language/fn-namespaces.md [anonymous function]: {{rootUrl}}/language/fn-anon.md diff --git a/doc/src/start/features.md b/doc/src/start/features.md index 0f6b0f53..7a06618a 100644 --- a/doc/src/start/features.md +++ b/doc/src/start/features.md @@ -23,6 +23,7 @@ more control over what a script can (or cannot) do. | `no_object` | Disable support for [custom types] and [object maps]. | | `no_function` | Disable script-defined [functions]. | | `no_module` | Disable loading external [modules]. | +| `no_capture` | Disable capturing external variables in [anonymous functions]. | | `no_std` | Build for `no-std`. Notice that additional dependencies will be pulled in to replace `std` features. | | `serde` | Enable serialization/deserialization via `serde`. Notice that the [`serde`](https://crates.io/crates/serde) crate will be pulled in together with its dependencies. | | `internals` | Expose internal data structures (e.g. [`AST`] nodes). Beware that Rhai internals are volatile and may change from version to version. | diff --git a/tests/call_fn.rs b/tests/call_fn.rs index d603f743..0f9087d2 100644 --- a/tests/call_fn.rs +++ b/tests/call_fn.rs @@ -83,38 +83,6 @@ fn test_call_fn_private() -> Result<(), Box> { Ok(()) } -#[test] -fn test_anonymous_fn() -> Result<(), Box> { - let calc_func = Func::<(INT, INT, INT), INT>::create_from_script( - Engine::new(), - "fn calc(x, y, z,) { (x + y) * z }", - "calc", - )?; - - assert_eq!(calc_func(42, 123, 9)?, 1485); - - let calc_func = Func::<(INT, String, INT), INT>::create_from_script( - Engine::new(), - "fn calc(x, y, z) { (x + len(y)) * z }", - "calc", - )?; - - assert_eq!(calc_func(42, "hello".to_string(), 9)?, 423); - - let calc_func = Func::<(INT, INT, INT), INT>::create_from_script( - Engine::new(), - "private fn calc(x, y, z) { (x + y) * z }", - "calc", - )?; - - assert!(matches!( - *calc_func(42, 123, 9).expect_err("should error"), - EvalAltResult::ErrorFunctionNotFound(fn_name, _) if fn_name == "calc" - )); - - Ok(()) -} - #[test] #[cfg(not(feature = "no_object"))] fn test_fn_ptr_raw() -> Result<(), Box> { @@ -179,58 +147,33 @@ fn test_fn_ptr_raw() -> Result<(), Box> { } #[test] -fn test_fn_ptr_curry_call() -> Result<(), Box> { - let mut module = Module::new(); +fn test_anonymous_fn() -> Result<(), Box> { + let calc_func = Func::<(INT, INT, INT), INT>::create_from_script( + Engine::new(), + "fn calc(x, y, z,) { (x + y) * z }", + "calc", + )?; - module.set_raw_fn( - "call_with_arg", - &[TypeId::of::(), TypeId::of::()], - |engine: &Engine, lib: &Module, args: &mut [&mut Dynamic]| { - let fn_ptr = std::mem::take(args[0]).cast::(); - fn_ptr.call_dynamic(engine, lib, None, [std::mem::take(args[1])]) - }, - ); + assert_eq!(calc_func(42, 123, 9)?, 1485); - let mut engine = Engine::new(); - engine.load_package(module.into()); + let calc_func = Func::<(INT, String, INT), INT>::create_from_script( + Engine::new(), + "fn calc(x, y, z) { (x + len(y)) * z }", + "calc", + )?; - #[cfg(not(feature = "no_object"))] - assert_eq!( - engine.eval::( - r#" - let addition = |x, y| { x + y }; - let curried = addition.curry(2); + assert_eq!(calc_func(42, "hello".to_string(), 9)?, 423); - call_with_arg(curried, 40) - "# - )?, - 42 - ); - - Ok(()) -} - -#[test] -#[cfg(not(feature = "no_capture"))] -fn test_fn_closures() -> Result<(), Box> { - let engine = Engine::new(); - - assert_eq!( - engine.eval::( - r#" - let x = 8; - - let res = |y, z| { - let w = 12; - - return (|| x + y + z + w).call(); - }.curry(15).call(2); - - res + (|| x - 3).call() - "# - )?, - 42 - ); + let calc_func = Func::<(INT, INT, INT), INT>::create_from_script( + Engine::new(), + "private fn calc(x, y, z) { (x + y) * z }", + "calc", + )?; + + assert!(matches!( + *calc_func(42, 123, 9).expect_err("should error"), + EvalAltResult::ErrorFunctionNotFound(fn_name, _) if fn_name == "calc" + )); Ok(()) } diff --git a/tests/closures.rs b/tests/closures.rs new file mode 100644 index 00000000..3aec4865 --- /dev/null +++ b/tests/closures.rs @@ -0,0 +1,60 @@ +#![cfg(not(feature = "no_function"))] +use rhai::{Dynamic, Engine, EvalAltResult, FnPtr, Module, INT}; +use std::any::TypeId; + +#[test] +fn test_fn_ptr_curry_call() -> Result<(), Box> { + let mut module = Module::new(); + + module.set_raw_fn( + "call_with_arg", + &[TypeId::of::(), TypeId::of::()], + |engine: &Engine, lib: &Module, args: &mut [&mut Dynamic]| { + let fn_ptr = std::mem::take(args[0]).cast::(); + fn_ptr.call_dynamic(engine, lib, None, [std::mem::take(args[1])]) + }, + ); + + let mut engine = Engine::new(); + engine.load_package(module.into()); + + #[cfg(not(feature = "no_object"))] + assert_eq!( + engine.eval::( + r#" + let addition = |x, y| { x + y }; + let curried = addition.curry(2); + + call_with_arg(curried, 40) + "# + )?, + 42 + ); + + Ok(()) +} + +#[test] +#[cfg(not(feature = "no_capture"))] +fn test_closures() -> Result<(), Box> { + let engine = Engine::new(); + + assert_eq!( + engine.eval::( + r#" + let x = 8; + + let res = |y, z| { + let w = 12; + + return (|| x + y + z + w).call(); + }.curry(15).call(2); + + res + (|| x - 3).call() + "# + )?, + 42 + ); + + Ok(()) +}