dart-lang / site-www

Source for Dart website
https://dart.dev
Other
968 stars 700 forks source link

Extend example code for "lexical closures" on 'Functions' page #5735

Open dtonhofer opened 6 months ago

dtonhofer commented 6 months ago

Page URL

https://dart.dev/language/functions/

Page source

https://github.com/dart-lang/site-www/tree/main/src/content/language/functions.md

Describe the problem

At

https://dart.dev/language/functions#lexical-closures

we are shown a code snippet showing a higher-order function creating an "adder function".

I feel as if the typing part is getting a bit of a short shrift as the higher-order function makeAdder() has return type Function, which is a bit general. (I discovered it can be modified to return dynamic with good results, or else to return Object but then I can't do anything with the object, but that's just by the by).

Suggesting to show an alternate makeAdder2() which further specifies what is being returned. Like this:

/// Higher-order function that returns a function that adds [addBy] 
/// to the function's argument.
Function makeAdder(int addBy) {
  return (int i) => addBy + i;
}

/// Higher-order function that returns a function that adds [addBy] 
/// to the function's argument.
/// This one declares what is being returned in detail!
/// We also show two ways of definition the function that is returned.
int Function(int) makeAdder2(int addBy) {
  // var f = (int i) => (addBy + i);  // Ok, and you can reassign f.
  f(int i) => (addBy + i); // Preferred by linter; f is not re-assignable.
  print("The type of the returned function is ${f.runtimeType}");
  return f;
}

void main() {
  {
    // Create a function that adds 2.
    final add2 = makeAdder(2);

    // Create a function that adds 4.
    final add4 = makeAdder(4);

    assert(add2(3) == 5);
    assert(add4(3) == 7);
  }
  // Alternatively, call the returned function directly.
  assert(makeAdder(2)(3) == 5);
  assert(makeAdder(4)(3) == 7);
  // This prints "The type of the function to be returned is (int) => int".
  // Note that (int) => int cannot, however, be used to declare the return type
  // of the function,
  assert(makeAdder2(2)(3) == 5);
}

Expected fix

No response

Additional context

No response

I would like to fix this problem.

dtonhofer commented 6 months ago

More generally, there might be room for a section on "how to declare parameters that are typed as functions", as it is not immediately clear how to do that. Here is some example code:

// A procedure taking a function taking no arguments and returning anything

void bar1(dynamic Function() body) {
  print('bar1() called with ${body.runtimeType}');
  body();
}

// The same as above but with deprecated syntax. The linter will warn!

void bar2(dynamic body()) {
  print('bar2() called with ${body.runtimeType}');
  body();
}

// This also works: instead of "dynamic" return a nullable Object.
// However, in the case of "dynamic" the compiler will do type inference,
// but if you return "Object?" you actually fix the type of the returned 
// value.

void bar3(Object? Function() body) {
  print('bar3() called with ${body.runtimeType}');
  body();
}

void callingBar() {
  bar1(() => 6); // () => int
  bar2(() => 6); // () => int
  bar3(() => 6); // () => int
  bar1(() => null); // () => Null (null is the single inhabitant of type Null)
  bar1(() {}); // () => Null (even if the body does not return anything)
  bar1(() {
    return;
  }); // () => Null (even if the body does not return anything)
}

// ---------------

// A procedure taking a function taking no arguments and returning void.

void foo1(void Function() body) {
  print('foo1() called with ${body.runtimeType}');
  body();
}

// The same as above but with deprecated syntax. The linter will warn.

void foo2(void body()) {
  print('foo2() called with ${body.runtimeType}');
  body();
}

void callingFoo() {
  foo1(() => 6); // () => void
  foo2(() => 6); // () => void
  foo1(() {}); // () => void
  foo1(() {
    return;
  }); // () => void
}

// linter warns: "don't return null from a function returning void"

void callingFooDubiously() {
  foo1(() => null); // () => void
}

// ---------------

// A procedure taking a function taking no arguments and returning bool.

void baz1(bool Function() body) {
  print('baz1() called with ${body.runtimeType}');
  body();
}

// The same as above but with deprecated syntax. The linter will warn.

void baz2(bool body()) {
  print('baz2() called with ${body.runtimeType}');
  body();
}

void callingBaz() {
  baz1(() => true); // () => bool
  baz2(() => true); // () => bool
  baz1(() {
    return true;
  }); // () => bool
}

// ---------------

// A procedure taking a function taking an int and returning bool.

void quux1(bool Function(int x) body) {
  print('quux1() called with ${body.runtimeType}');
  body(12);
}

// Same as above

void quux2(bool Function(int) body) {
  print('quux2() called with ${body.runtimeType}');
  body(12);
}

// The same as above but with deprecated syntax. The linter will warn.

void quux3(bool body(int x)) {
  print('quux3() called with ${body.runtimeType}');
  body(12);
}

// Not the same as above!! The token 'int' is not interpreted
// as a type: "body" takes a "dynamic", not an "int"!

void quux4(bool body(int)) {
  print('quux4() called with ${body.runtimeType}');
  body(12);
}

void callingQuux() {
  quux1((x) => true); // (int) => bool
  quux1((int x) => true); // (int) => bool
  quux1((int x) {
    // (int) => bool
    return true;
  });
  quux2((x) => true); // (int) => bool
  quux2((int x) => true); // (int) => bool
  quux3((x) => true); // (int) => bool
  quux3((int x) => true); // (int) => bool
  quux4((x) => true); // (dynamic) => bool (UNEXPECTED!!)
  // quux4((int x) => true); // Does not compile
}

// ---------------

// A procedure taking a function taking an int and a named String
// and returning bool.

void alf1(bool Function(int, {String z}) body) {
  print('alf1() called with ${body.runtimeType}');
  body(12, z: "cat");
}

// The same as above but with deprecated syntax. The linter will warn.

void alf2(bool body(int i, {String z})) {
  print('alf2() called with ${body.runtimeType}');
  body(12, z: "cat");
}

// Not the same as above! The token 'int' is not interpreted
// as a type: "body" takes a "dynamic" and a name String.

void alf3(bool body(int, {String z})) {
  print('alf3() called with ${body.runtimeType}');
  body("hello", z: "cat");
}

void callingAlf() {
  alf1((x, {z = "hello"}) => true); // (int, {String z}) => bool
  alf2((x, {z = "hello"}) => true); // (int, {String z}) => bool
  alf3((x, {z = "hello"}) => true); // (dynamic, {String z}) => bool
}

// ---------------

// comparisons

void moo(bool Function(int x, {String z}) body1,
    bool Function(int x, {String z}) body2) {
  print('moo() called with ${body1.runtimeType} and ${body2.runtimeType}');
  print('Are the types equal? ${body1.runtimeType == body2.runtimeType}');
}

void callingMoo() {
  moo((x, {z = "hello"}) => true, (x, {z = "world"}) => false);
}

void woo(bool Function(int x, {String z}) body1,
    bool Function(double x, {String z}) body2) {
  print('woo() called with ${body1.runtimeType} and ${body2.runtimeType}');
  print('Are the types equal? ${body1.runtimeType == body2.runtimeType}');
}

void callingWoo() {
  woo((x, {z = "hello"}) => true, (x, {z = "world"}) => false);
}

void main() {
  callingBar();
  callingFoo();
  callingFooDubiously();
  callingBaz();
  callingQuux();
  callingAlf();
  callingMoo();
  callingWoo();
  makeAdder(12);
  print('${makeAdder(12)(11)}');
}

With the output:

bar1() called with () => int
bar2() called with () => int
bar3() called with () => int
bar1() called with () => Null
bar1() called with () => Null
bar1() called with () => Null
foo1() called with () => void
foo2() called with () => void
foo1() called with () => void
foo1() called with () => void
foo1() called with () => void
baz1() called with () => bool
baz2() called with () => bool
baz1() called with () => bool
quux1() called with (int) => bool
quux1() called with (int) => bool
quux1() called with (int) => bool
quux2() called with (int) => bool
quux2() called with (int) => bool
quux3() called with (int) => bool
quux3() called with (int) => bool
quux4() called with (dynamic) => bool
alf1() called with (int, {String z}) => bool
alf2() called with (int, {String z}) => bool
alf3() called with (dynamic, {String z}) => bool
moo() called with (int, {String z}) => bool and (int, {String z}) => bool
Are the types equal? true
woo() called with (int, {String z}) => bool and (double, {String z}) => bool
Are the types equal? false