generate: switch the position of <initial> and <closure>, so the closure can have default parameters (#13393)

# Description
Close: #12083 
Close: #12084 

# User-Facing Changes
It's a breaking change because we have switched the position of
`<initial>` and `<closure>`, after the change, initial value will be
optional. So it's possible to do something like this:

```nushell
> let f = {|fib = [0, 1]| {out: $fib.0, next: [$fib.1, ($fib.0 + $fib.1)]} }
> generate $f | first 5
╭───┬───╮
│ 0 │ 0 │
│ 1 │ 1 │
│ 2 │ 1 │
│ 3 │ 2 │
│ 4 │ 3 │
╰───┴───╯
```

It will also raise error if user don't give initial value, and the
closure don't have default parameter.
```nushell
❯ let f = {|fib| {out: $fib.0, next: [$fib.1, ($fib.0 + $fib.1)]} }
❯ generate $f
Error:   × The initial value is missing
   ╭─[entry #5:1:1]
 1 │ generate $f
   · ────┬───
   ·     ╰── Missing intial value
   ╰────
  help: Provide <initial> value in generate, or assigning default value to closure parameter

```

# Tests + Formatting
Added some test cases.

---------

Co-authored-by: Stefan Holderbach <sholderbach@users.noreply.github.com>
This commit is contained in:
Wind 2024-07-19 15:22:28 +08:00 committed by GitHub
parent e8764de3c6
commit e281c03403
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 117 additions and 31 deletions

View File

@ -12,12 +12,12 @@ impl Command for Generate {
fn signature(&self) -> Signature { fn signature(&self) -> Signature {
Signature::build("generate") Signature::build("generate")
.input_output_types(vec![(Type::Nothing, Type::List(Box::new(Type::Any)))]) .input_output_types(vec![(Type::Nothing, Type::List(Box::new(Type::Any)))])
.required("initial", SyntaxShape::Any, "Initial value.")
.required( .required(
"closure", "closure",
SyntaxShape::Closure(Some(vec![SyntaxShape::Any])), SyntaxShape::Closure(Some(vec![SyntaxShape::Any])),
"Generator function.", "Generator function.",
) )
.optional("initial", SyntaxShape::Any, "Initial value.")
.allow_variants_without_examples(true) .allow_variants_without_examples(true)
.category(Category::Generators) .category(Category::Generators)
} }
@ -41,7 +41,7 @@ used as the next argument to the closure, otherwise generation stops.
fn examples(&self) -> Vec<Example> { fn examples(&self) -> Vec<Example> {
vec![ vec![
Example { Example {
example: "generate 0 {|i| if $i <= 10 { {out: $i, next: ($i + 2)} }}", example: "generate {|i| if $i <= 10 { {out: $i, next: ($i + 2)} }} 0",
description: "Generate a sequence of numbers", description: "Generate a sequence of numbers",
result: Some(Value::list( result: Some(Value::list(
vec![ vec![
@ -57,10 +57,17 @@ used as the next argument to the closure, otherwise generation stops.
}, },
Example { Example {
example: example:
"generate [0, 1] {|fib| {out: $fib.0, next: [$fib.1, ($fib.0 + $fib.1)]} }", "generate {|fib| {out: $fib.0, next: [$fib.1, ($fib.0 + $fib.1)]} } [0, 1]",
description: "Generate a continuous stream of Fibonacci numbers", description: "Generate a continuous stream of Fibonacci numbers",
result: None, result: None,
}, },
Example {
example:
"generate {|fib=[0, 1]| {out: $fib.0, next: [$fib.1, ($fib.0 + $fib.1)]} }",
description:
"Generate a continuous stream of Fibonacci numbers, using default parameters",
result: None,
},
] ]
} }
@ -72,15 +79,15 @@ used as the next argument to the closure, otherwise generation stops.
_input: PipelineData, _input: PipelineData,
) -> Result<PipelineData, ShellError> { ) -> Result<PipelineData, ShellError> {
let head = call.head; let head = call.head;
let initial: Value = call.req(engine_state, stack, 0)?; let closure: Closure = call.req(engine_state, stack, 0)?;
let closure: Closure = call.req(engine_state, stack, 1)?; let initial: Option<Value> = call.opt(engine_state, stack, 1)?;
let block = engine_state.get_block(closure.block_id);
let mut closure = ClosureEval::new(engine_state, stack, closure); let mut closure = ClosureEval::new(engine_state, stack, closure);
// A type of Option<S> is used to represent state. Invocation // A type of Option<S> is used to represent state. Invocation
// will stop on None. Using Option<S> allows functions to output // will stop on None. Using Option<S> allows functions to output
// one final value before stopping. // one final value before stopping.
let mut state = Some(initial); let mut state = Some(get_initial_state(initial, &block.signature, call.head)?);
let iter = std::iter::from_fn(move || { let iter = std::iter::from_fn(move || {
let arg = state.take()?; let arg = state.take()?;
@ -170,6 +177,38 @@ used as the next argument to the closure, otherwise generation stops.
} }
} }
fn get_initial_state(
initial: Option<Value>,
signature: &Signature,
span: Span,
) -> Result<Value, ShellError> {
match initial {
Some(v) => Ok(v),
None => {
// the initial state should be referred from signature
if !signature.optional_positional.is_empty()
&& signature.optional_positional[0].default_value.is_some()
{
Ok(signature.optional_positional[0]
.default_value
.clone()
.expect("Already checked default value"))
} else {
Err(ShellError::GenericError {
error: "The initial value is missing".to_string(),
msg: "Missing initial value".to_string(),
span: Some(span),
help: Some(
"Provide an <initial> value as an argument to generate, or assign a default value to the closure parameter"
.to_string(),
),
inner: vec![],
})
}
}
}
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;

View File

@ -3,7 +3,7 @@ use nu_test_support::{nu, pipeline};
#[test] #[test]
fn generate_no_next_break() { fn generate_no_next_break() {
let actual = nu!( let actual = nu!(
"generate 1 {|x| if $x == 3 { {out: $x}} else { {out: $x, next: ($x + 1)} }} | to nuon" "generate {|x| if $x == 3 { {out: $x}} else { {out: $x, next: ($x + 1)} }} 1 | to nuon"
); );
assert_eq!(actual.out, "[1, 2, 3]"); assert_eq!(actual.out, "[1, 2, 3]");
@ -11,7 +11,7 @@ fn generate_no_next_break() {
#[test] #[test]
fn generate_null_break() { fn generate_null_break() {
let actual = nu!("generate 1 {|x| if $x <= 3 { {out: $x, next: ($x + 1)} }} | to nuon"); let actual = nu!("generate {|x| if $x <= 3 { {out: $x, next: ($x + 1)} }} 1 | to nuon");
assert_eq!(actual.out, "[1, 2, 3]"); assert_eq!(actual.out, "[1, 2, 3]");
} }
@ -20,13 +20,13 @@ fn generate_null_break() {
fn generate_allows_empty_output() { fn generate_allows_empty_output() {
let actual = nu!(pipeline( let actual = nu!(pipeline(
r#" r#"
generate 0 {|x| generate {|x|
if $x == 1 { if $x == 1 {
{next: ($x + 1)} {next: ($x + 1)}
} else if $x < 3 { } else if $x < 3 {
{out: $x, next: ($x + 1)} {out: $x, next: ($x + 1)}
} }
} | to nuon } 0 | to nuon
"# "#
)); ));
@ -37,11 +37,11 @@ fn generate_allows_empty_output() {
fn generate_allows_no_output() { fn generate_allows_no_output() {
let actual = nu!(pipeline( let actual = nu!(pipeline(
r#" r#"
generate 0 {|x| generate {|x|
if $x < 3 { if $x < 3 {
{next: ($x + 1)} {next: ($x + 1)}
} }
} | to nuon } 0 | to nuon
"# "#
)); ));
@ -52,7 +52,7 @@ fn generate_allows_no_output() {
fn generate_allows_null_state() { fn generate_allows_null_state() {
let actual = nu!(pipeline( let actual = nu!(pipeline(
r#" r#"
generate 0 {|x| generate {|x|
if $x == null { if $x == null {
{out: "done"} {out: "done"}
} else if $x < 1 { } else if $x < 1 {
@ -60,7 +60,7 @@ fn generate_allows_null_state() {
} else { } else {
{out: "stopping", next: null} {out: "stopping", next: null}
} }
} | to nuon } 0 | to nuon
"# "#
)); ));
@ -71,7 +71,42 @@ fn generate_allows_null_state() {
fn generate_allows_null_output() { fn generate_allows_null_output() {
let actual = nu!(pipeline( let actual = nu!(pipeline(
r#" r#"
generate 0 {|x| generate {|x|
if $x == 3 {
{out: "done"}
} else {
{out: null, next: ($x + 1)}
}
} 0 | to nuon
"#
));
assert_eq!(actual.out, "[null, null, null, done]");
}
#[test]
fn generate_disallows_extra_keys() {
let actual = nu!("generate {|x| {foo: bar, out: $x}} 0 ");
assert!(actual.err.contains("Invalid block return"));
}
#[test]
fn generate_disallows_list() {
let actual = nu!("generate {|x| [$x, ($x + 1)]} 0 ");
assert!(actual.err.contains("Invalid block return"));
}
#[test]
fn generate_disallows_primitive() {
let actual = nu!("generate {|x| 1} 0");
assert!(actual.err.contains("Invalid block return"));
}
#[test]
fn generate_allow_default_parameter() {
let actual = nu!(pipeline(
r#"
generate {|x = 0|
if $x == 3 { if $x == 3 {
{out: "done"} {out: "done"}
} else { } else {
@ -82,22 +117,34 @@ fn generate_allows_null_output() {
)); ));
assert_eq!(actual.out, "[null, null, null, done]"); assert_eq!(actual.out, "[null, null, null, done]");
// if initial is given, use initial value
let actual = nu!(pipeline(
r#"
generate {|x = 0|
if $x == 3 {
{out: "done"}
} else {
{out: null, next: ($x + 1)}
}
} 1 | to nuon
"#
));
assert_eq!(actual.out, "[null, null, done]");
} }
#[test] #[test]
fn generate_disallows_extra_keys() { fn generate_raise_error_on_no_default_parameter_closure_and_init_val() {
let actual = nu!("generate 0 {|x| {foo: bar, out: $x}}"); let actual = nu!(pipeline(
assert!(actual.err.contains("Invalid block return")); r#"
} generate {|x|
if $x == 3 {
#[test] {out: "done"}
fn generate_disallows_list() { } else {
let actual = nu!("generate 0 {|x| [$x, ($x + 1)]}"); {out: null, next: ($x + 1)}
assert!(actual.err.contains("Invalid block return")); }
} } | to nuon
"#
#[test] ));
fn generate_disallows_primitive() { assert!(actual.err.contains("The initial value is missing"));
let actual = nu!("generate 0 {|x| 1}");
assert!(actual.err.contains("Invalid block return"));
} }