Add patterns section.

This commit is contained in:
Stephen Chung
2020-08-07 11:44:15 +08:00
parent 5e6d5e8e80
commit 0b21d80641
9 changed files with 382 additions and 12 deletions

103
doc/src/patterns/config.md Normal file
View File

@@ -0,0 +1,103 @@
Loadable Configuration
======================
{{#include ../links.md}}
Usage Scenario
--------------
* A system where settings and configurations are complex and logic-driven.
* Where it is not possible to configure said system via standard configuration file formats such as `TOML` or `YAML`.
* The system configuration is complex enough that it requires a full programming language. Essentially _configuration by code_.
* Yet the configurations must be flexible, late-bound and dynamically loadable, just like a configuration file.
Key Concepts
------------
* Leverage the loadable [modules] of Rhai. The [`no_module`] feature must not be on.
* Expose the configuration API. Use separate scripts to configure that API. Dynamically load scripts via the `import` statement.
* Since Rhai is _sand-boxed_, it cannot mutate the environment. To modify the external configuration object via an API, it must be wrapped in a `RefCell` (or `RwLock`/`Mutex` for [`sync`]) and shared to the [`Engine`].
Implementation
--------------
### Configuration Type
```rust
#[derive(Debug, Clone, Default)]
struct Config {
pub id: String;
pub some_field: i64;
pub some_list: Vec<String>;
pub some_map: HashMap<String, bool>;
}
```
### Make Shared Object
```rust
let config: Rc<RefCell<Config>> = Rc::new(RefCell::(Default::default()));
```
### Register Config API
```rust
// Notice 'move' is used to move the shared configuration object into the closure.
let cfg = config.clone();
engine.register_fn("config_set_id", move |id: String| *cfg.borrow_mut().id = id);
let cfg = config.clone();
engine.register_fn("config_get_id", move || cfg.borrow().id.clone());
let cfg = config.clone();
engine.register_fn("config_set", move |value: i64| *cfg.borrow_mut().some_field = value);
// Remember Rhai functions can be overloaded when designing the API.
let cfg = config.clone();
engine.register_fn("config_add", move |value: String|
cfg.borrow_mut().some_list.push(value)
);
let cfg = config.clone();
engine.register_fn("config_add", move |values: &mut Array|
cfg.borrow_mut().some_list.extend(values.into_iter().map(|v| v.to_string()))
);
let cfg = config.clone();
engine.register_fn("config_add", move |key: String, value: bool|
cfg.borrow_mut().som_map.insert(key, value)
);
```
### Configuration Script
```rust
------------------
| my_config.rhai |
------------------
config_set_id("hello");
config_add("foo"); // add to list
config_add("bar", true); // add to map
config_add("baz", false); // add to map
```
### Load the Configuration
```rust
import "my_config"; // run configuration script without creating a module
let id = config_get_id();
id == "hello";
```

117
doc/src/patterns/control.md Normal file
View File

@@ -0,0 +1,117 @@
Scriptable Control Layer
========================
{{#include ../links.md}}
Usage Scenario
--------------
* A system provides core functionalities, but no driving logic.
* The driving logic must be dynamic and hot-loadable.
* A script is used to drive the system and provide control intelligence.
Key Concepts
------------
* Expose a Control API.
* Since Rhai is _sand-boxed_, it cannot mutate the environment. To perform external actions via an API, the actual system must be wrapped in a `RefCell` (or `RwLock`/`Mutex` for [`sync`]) and shared to the [`Engine`].
Implementation
--------------
### Functional API
Assume that a system provides the following functional API:
```rust
struct EnergizerBunny;
impl EnergizerBunny {
pub fn new () -> Self { ... }
pub fn go (&mut self) { ... }
pub fn stop (&mut self) { ... }
pub fn is_going (&self) { ... }
pub fn get_speed (&self) -> i64 { ... }
pub fn set_speed (&mut self, speed: i64) { ... }
}
```
### Wrap API in Shared Object
```rust
let bunny: Rc<RefCell<EnergizerBunny>> = Rc::new(RefCell::(EnergizerBunny::new()));
```
### Register Control API
```rust
// Notice 'move' is used to move the shared API object into the closure.
let b = bunny.clone();
engine.register_fn("bunny_power", move |on: bool| {
if on {
if b.borrow().is_going() {
println!("Still going...");
} else {
b.borrow_mut().go();
}
} else {
if b.borrow().is_going() {
b.borrow_mut().stop();
} else {
println!("Already out of battery!");
}
}
});
let b = bunny.clone();
engine.register_fn("bunny_is_going", move || b.borrow().is_going());
let b = bunny.clone();
engine.register_fn("bunny_get_speed", move ||
if b.borrow().is_going() { b.borrow().get_speed() } else { 0 }
);
let b = bunny.clone();
engine.register_result_fn("bunny_set_speed", move |speed: i64|
if speed <= 0 {
return Err("Speed must be positive!".into());
} else if speed > 100 {
return Err("Bunny will be going too fast!".into());
}
if b.borrow().is_going() {
b.borrow_mut().set_speed(speed)
} else {
return Err("Bunny is not yet going!".into());
}
Ok(().into())
);
```
### Use the API
```rust
if !bunny_is_going() { bunny_power(true); }
if bunny_get_speed() > 50 { bunny_set_speed(50); }
```
Caveat
------
Although this usage pattern appears a perfect fit for _game_ logic, avoid writing the
_entire game_ in Rhai. Performance will not be acceptable.
Implement as much functionalities of the game engine in Rust as possible.
Rhai integrates well with Rust so this is usually not a hinderance.
Lift as much out of Rhai as possible.
Use Rhai only for the logic that _must_ be dynamic or hot-loadable.

View File

@@ -0,0 +1,9 @@
Advanced Patterns
=================
{{#include ../links.md}}
Use Rhai in different scenarios other than simply evaluating a user script.
These patterns are useful when Rhai needs to affect/control the external environment.

62
doc/src/patterns/oop.md Normal file
View File

@@ -0,0 +1,62 @@
Object-Oriented Programming (OOP)
================================
{{#include ../links.md}}
Rhai does not have _objects_ per se, but it is possible to _simulate_ object-oriented programming.
Use [Object Maps] to Simulate OOP
--------------------------------
Rhai's [object maps] has [special support for OOP]({{rootUrl}}/language/object-maps-oop.md).
| Rhai concept | Maps to OOP |
| ----------------------------------------------------- | :---------: |
| [Object maps] | objects |
| [Object map] properties holding values | properties |
| [Object map] properties that hold [function pointers] | methods |
When a property of an [object map] is called like a method function, and if it happens to hold
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_closure`] feature.
Examples
--------
```rust
let factor = 1;
// Define the object
let obj =
#{
data: 0,
increment: |x| this.data += x, // 'this' binds to 'obj'
update: |x| this.data = x * factor, // 'this' binds to 'obj', 'factor' is captured
action: || print(this.data) // 'this' binds to 'obj'
};
// Use the object
obj.increment(1);
obj.action(); // prints 1
obj.update(42);
obj.action(); // prints 42
factor = 2;
obj.update(42);
obj.action(); // prints 84
```

View File

@@ -0,0 +1,136 @@
Singleton Command Objects
========================
{{#include ../links.md}}
Usage Scenario
--------------
* A system provides core functionalities, but no driving logic.
* The driving logic must be dynamic and hot-loadable.
* A script is used to drive the system and provide control intelligence.
* The API is multiplexed, meaning that it can act on multiple system-provided entities, or
* The API lends itself readily to an object-oriented (OO) representation.
Key Concepts
------------
* Expose a Command type with an API.
* Since Rhai is _sand-boxed_, it cannot mutate the environment. To perform external actions via an API, the command object type must be wrapped in a `RefCell` (or `RwLock`/`Mutex` for [`sync`]) and shared to the [`Engine`].
* Load each command object into a custom [`Scope`] as constant variables.
* Control each command object in script via the constants.
Implementation
--------------
### Functional API
Assume the following command object type:
```rust
struct EnergizerBunny { ... }
impl EnergizerBunny {
pub fn new () -> Self { ... }
pub fn go (&mut self) { ... }
pub fn stop (&mut self) { ... }
pub fn is_going (&self) { ... }
pub fn get_speed (&self) -> i64 { ... }
pub fn set_speed (&mut self, speed: i64) { ... }
pub fn turn (&mut self, left_turn: bool) { ... }
}
```
### Wrap Command Object Type as Shared
```rust
let SharedBunnyType = Rc<RefCell<EnergizerBunny>>;
```
### Register the Custom Type
```rust
engine.register_type_with_name::<SharedBunnyType>("EnergizerBunny");
```
### Register Methods and Getters/Setters
```rust
engine
.register_get_set("power",
|bunny: &mut SharedBunnyType| bunny.borrow().is_going(),
|bunny: &mut SharedBunnyType, on: bool| {
if on {
if bunny.borrow().is_going() {
println!("Still going...");
} else {
bunny.borrow_mut().go();
}
} else {
if bunny.borrow().is_going() {
bunny.borrow_mut().stop();
} else {
println!("Already out of battery!");
}
}
}
).register_get("speed", |bunny: &mut SharedBunnyType| {
if bunny.borrow().is_going() {
bunny.borrow().get_speed()
} else {
0
}
}).register_set_result("speed", |bunny: &mut SharedBunnyType, speed: i64| {
if speed <= 0 {
Err("Speed must be positive!".into())
} else if speed > 100 {
Err("Bunny will be going too fast!".into())
} else if !bunny.borrow().is_going() {
Err("Bunny is not yet going!".into())
} else {
b.borrow_mut().set_speed(speed);
Ok(().into())
}
}).register_fn("turn_left", |bunny: &mut SharedBunnyType| {
if bunny.borrow().is_going() {
bunny.borrow_mut().turn(true);
}
}).register_fn("turn_right", |bunny: &mut SharedBunnyType| {
if bunny.borrow().is_going() {
bunny.borrow_mut().turn(false);
}
});
```
### Push Constant Command Object into Custom Scope
```rust
let bunny: SharedBunnyType = Rc::new(RefCell::(EnergizerBunny::new()));
let mut scope = Scope::new();
scope.push_constant("BUNNY", bunny.clone());
engine.consume_with_scope(&mut scope, script)?;
```
### Use the Command API in Script
```rust
// Access the command object via constant variable 'BUNNY'.
if !BUNNY.power { BUNNY.power = true; }
if BUNNY.speed > 50 { BUNNY.speed = 50; }
BUNNY.turn_left();
```