Add docs for auto-currying.

This commit is contained in:
Stephen Chung 2020-07-29 22:43:57 +08:00
parent 1465ba2315
commit 8299adf95c
10 changed files with 178 additions and 80 deletions

View File

@ -79,6 +79,7 @@ The Rhai Scripting Language
4. [Function Pointers](language/fn-ptr.md) 4. [Function Pointers](language/fn-ptr.md)
5. [Anonymous Functions](language/fn-anon.md) 5. [Anonymous Functions](language/fn-anon.md)
6. [Currying](language/fn-curry.md) 6. [Currying](language/fn-curry.md)
7. [Capturing External Variables](language/fn-closure.md)
16. [Print and Debug](language/print-debug.md) 16. [Print and Debug](language/print-debug.md)
17. [Modules](language/modules/index.md) 17. [Modules](language/modules/index.md)
1. [Export Variables, Functions and Sub-Modules](language/modules/export.md) 1. [Export Variables, Functions and Sub-Modules](language/modules/export.md)

View File

@ -15,6 +15,17 @@ The [`Engine::eval_expression_XXX`][`eval_expression`] API can be used to restri
a script to expressions only. 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 Disable Keywords and/or Operators
-------------------------------- --------------------------------

View File

@ -49,9 +49,13 @@ fn anon_fn_1001(x) { this.data -= x; }
fn anon_fn_1002() { print this.data; } fn anon_fn_1002() { print this.data; }
``` ```
WARNING - NOT Closures WARNING - NOT Closures
---------------------- ----------------------
Remember: anonymous functions, though having the same syntax as Rust _closures_, are themselves 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 **not** closures. In particular, they do not capture their running environment. They are more like
Rust's function pointers. 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].

View File

@ -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;
```

View File

@ -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)' curried.call(2) == 42; // <- de-sugars to 'func.call(21, 2)'
// only one argument is now required // 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].

View File

@ -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 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. 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 Examples
-------- --------

View File

@ -9,6 +9,7 @@
[`no_object`]: {{rootUrl}}/start/features.md [`no_object`]: {{rootUrl}}/start/features.md
[`no_function`]: {{rootUrl}}/start/features.md [`no_function`]: {{rootUrl}}/start/features.md
[`no_module`]: {{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
[`no-std`]: {{rootUrl}}/start/features.md [`no-std`]: {{rootUrl}}/start/features.md
[`internals`]: {{rootUrl}}/start/features.md [`internals`]: {{rootUrl}}/start/features.md
@ -78,6 +79,8 @@
[function pointer]: {{rootUrl}}/language/fn-ptr.md [function pointer]: {{rootUrl}}/language/fn-ptr.md
[function pointers]: {{rootUrl}}/language/fn-ptr.md [function pointers]: {{rootUrl}}/language/fn-ptr.md
[currying]: {{rootUrl}}/language/fn-curry.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 namespace]: {{rootUrl}}/language/fn-namespaces.md
[function namespaces]: {{rootUrl}}/language/fn-namespaces.md [function namespaces]: {{rootUrl}}/language/fn-namespaces.md
[anonymous function]: {{rootUrl}}/language/fn-anon.md [anonymous function]: {{rootUrl}}/language/fn-anon.md

View File

@ -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_object` | Disable support for [custom types] and [object maps]. |
| `no_function` | Disable script-defined [functions]. | | `no_function` | Disable script-defined [functions]. |
| `no_module` | Disable loading external [modules]. | | `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. | | `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. | | `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. | | `internals` | Expose internal data structures (e.g. [`AST`] nodes). Beware that Rhai internals are volatile and may change from version to version. |

View File

@ -83,38 +83,6 @@ fn test_call_fn_private() -> Result<(), Box<EvalAltResult>> {
Ok(()) Ok(())
} }
#[test]
fn test_anonymous_fn() -> Result<(), Box<EvalAltResult>> {
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] #[test]
#[cfg(not(feature = "no_object"))] #[cfg(not(feature = "no_object"))]
fn test_fn_ptr_raw() -> Result<(), Box<EvalAltResult>> { fn test_fn_ptr_raw() -> Result<(), Box<EvalAltResult>> {
@ -179,58 +147,33 @@ fn test_fn_ptr_raw() -> Result<(), Box<EvalAltResult>> {
} }
#[test] #[test]
fn test_fn_ptr_curry_call() -> Result<(), Box<EvalAltResult>> { fn test_anonymous_fn() -> Result<(), Box<EvalAltResult>> {
let mut module = Module::new(); 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( assert_eq!(calc_func(42, 123, 9)?, 1485);
"call_with_arg",
&[TypeId::of::<FnPtr>(), TypeId::of::<INT>()],
|engine: &Engine, lib: &Module, args: &mut [&mut Dynamic]| {
let fn_ptr = std::mem::take(args[0]).cast::<FnPtr>();
fn_ptr.call_dynamic(engine, lib, None, [std::mem::take(args[1])])
},
);
let mut engine = Engine::new(); let calc_func = Func::<(INT, String, INT), INT>::create_from_script(
engine.load_package(module.into()); Engine::new(),
"fn calc(x, y, z) { (x + len(y)) * z }",
"calc",
)?;
#[cfg(not(feature = "no_object"))] assert_eq!(calc_func(42, "hello".to_string(), 9)?, 423);
assert_eq!(
engine.eval::<INT>(
r#"
let addition = |x, y| { x + y };
let curried = addition.curry(2);
call_with_arg(curried, 40) let calc_func = Func::<(INT, INT, INT), INT>::create_from_script(
"# Engine::new(),
)?, "private fn calc(x, y, z) { (x + y) * z }",
42 "calc",
); )?;
Ok(()) assert!(matches!(
} *calc_func(42, 123, 9).expect_err("should error"),
EvalAltResult::ErrorFunctionNotFound(fn_name, _) if fn_name == "calc"
#[test] ));
#[cfg(not(feature = "no_capture"))]
fn test_fn_closures() -> Result<(), Box<EvalAltResult>> {
let engine = Engine::new();
assert_eq!(
engine.eval::<INT>(
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(()) Ok(())
} }

60
tests/closures.rs Normal file
View File

@ -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<EvalAltResult>> {
let mut module = Module::new();
module.set_raw_fn(
"call_with_arg",
&[TypeId::of::<FnPtr>(), TypeId::of::<INT>()],
|engine: &Engine, lib: &Module, args: &mut [&mut Dynamic]| {
let fn_ptr = std::mem::take(args[0]).cast::<FnPtr>();
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::<INT>(
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<EvalAltResult>> {
let engine = Engine::new();
assert_eq!(
engine.eval::<INT>(
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(())
}