dart-lang / language

Design of the Dart language
Other
2.65k stars 201 forks source link

allow const modifier in function parameter and return type for creating const Widget with the parameter #2373

Open alrajdev opened 2 years ago

alrajdev commented 2 years ago

I have an extension function on List<Widget> which inserts SizedBox between every child to set spacing, instead of manually putting SizedBox between widget. What if there were 100 widgets, i don't want to put 99 SizedBox manually.

Here's the extension:

extension ChildSpace on List<Widget> {
  /// returns children with SizedBox between for spacing
  List<Widget> setSpace({
    final double? height,
    final double? width,
  }) {
    if (isEmpty) return this;
    // add the first child
    final spacedChildren = [
      this[0],
    ];

    for (final child in sublist(1)) {
      spacedChildren.addAll([
        // add space between every child
        SizedBox(
          height: height,
          width: width,
        ),
        child,
      ]);
    }

    return spacedChildren;
  }
}

use:

Column(
  children: [
    const Text("A"),
    Text(aVariable),
    const Text("C"),
  ].setSpace(height: 10),
)

Here in the Column the inserted SizedBox are not const, so will be rebuild which is bad. Since both the height and width parameter of setSpace function is not const, I cannot use

const SizedBox(
   height: height,
   width: width,
)

So is there any way you can let us use const modifier in function parameter and return type?

List<Widget> setSpace({const double? height, const double? width})

like C/C++?

const in return type is useful when doing some math with the const parameters

Every answer is I get is "const in dart is not like other languages", yes so let us use the optimization dart wants us to do in every widget "prefer const with const constructor"

Or is there any reason or difficulties for not allowing const in parameters and return types?

lrhn commented 2 years ago

If we add a const modifier to parameter and return types of a function declaration, with the meaning that the corresponding argument and return expressions must be constant expressions, which is definitely possible, then it a affects function typing too. We'd have to allow const on the parameter and return types of function types too.

That complicates the type system. And parsing.

We'd have to define the subtype relation. Likely const Foo is a subtype of Foo, since it accepts a subset of the values. That means that

  const Foo Function(Foo) <: Foo Function(const Foo)

And

const int Function(int) f = ...;

is ambiguous: Are we declaring a const variable, or a mutable variable with a function type returning a const type?

So, we may have to allow int foo(covariant const Foo foo) ... to override int foo(Foo foo) ....

Slightly surprisingly, a const constructor will not have a const return type. If called as a function, it can return a new object. (Also because the syntax const Foo() ... might suggest that it returns a const Foo. People make that mistake today, and we don't even have const return types.)

I'm also worried about putting too much emphasis on objects being constant. The Dart language doesn't have any concept of "constant objects", only "constant expressions". At runtime, every value is just a value, you can't easily distinguish const Foo(1) from new Foo(1) without already having a copy of const Foo(1) to compare for identity. Being constant is not part of the object, and a final instance = Foo(1); will be just as immutable as const instance = Foo(1). There is no good reason to disallow using the first instance where "use the same value each time" is required. It's immutable, and it's the same value each time. You can't tell the difference.


For the problem at hand, you cannot use const for that. Since the height is not constant. It's a parameter, and even a const parameter is not a constant expression because it doesn't have the same value every time.

Just do:

List<Widget> setSpace({
    final double? height,
    final double? width,
}) {
  if (length < 2) return this;
  var box = SizedBox(
     height: height,
     width: width,
  );
  var result = <Widget>[
    first,
  ];
  for (var i = 1; i < length; i++) {
    result..add(box)..add(this[i]);
  }
  return result;
}

That at least reuses the same box for every occurrence in the same list.

If you want to reuse boxes between calls, you need to cache the objects.

alrajdev commented 2 years ago

First of all thank you for your time and the suggestion of reusing the same SizedBox, I didn't think of that.

Are we declaring a constvariable, or a mutable variable with a function type returning a const type?

const int Function(int) f = ...;

this is the only problem I see, since it is hard to choose whether it is a const int return type or a const f variable.

But still having const in paramter is not difficult right?

And definitely

int add(int a, int b)

is different than

int add(const int a, const int b)

first can be called with any int variable or literal where second is only callable with a constvariable or literal. Of course without constreturn type it is useless to call the add function with the const argument, since return type of that add function will not be constand we lose the parameter's const type.

So what can we do, but we should do something right?, I means since Flutter expects const for non changing Widgets, we need those const everywhere we build a Widget.

For now i can only think of wrapping the const with the return type.

const (const int) Function(const int, const int) f = ...;

although it is a little confusing, well we can learn right? i mean all those null-safety symbols with different languages using different symbols for same concept, we still learn it.

But in the end, i think the const everywhere is a must as long as Flutter expects const for Widget building optimization.

Leave no non-changing Widgets behind without const

ykmnkmi commented 2 years ago

why not do like async, allow const modifier, which mean that all arguments and return type is const:

int get zero const {
  return 0;
}

int add(int a, int b) const {
  return a + b;
}

Also constant functions can't be async.

PS: Like @immutable, you can use this function as usual with non constant values.

lrhn commented 2 years ago

If you want int add(const int a, const int b) ... to return a constant, we're closer to #2222 or generalized constant evaluation (like what @munificent was working on before macros).

The obvious generalization from allowing const on parameter types is to allow const on any type, so you can have a List<const Foo> or a function with type parameter <T extends const Object> which only accepts const types. Might seem a little too far fetched, but experience tells us that any type that can occur in a function type, someone also wants to abstract over it using generics. Meaning that this probably means const SomeType becomes a valid type in general, just like SomeType? (it's just a subtype of SomeType instead of a supertype). And yes, you will want Future<const Foo> compute() async ... too. That's definitely a lot of complication for the type system. (Grammar notwithstanding, we can do hacks like const<Foo> if we can't find something better.)

Levi-Lesches commented 2 years ago

The real gist here is that you don't need to pass width or height -- what you're really looking for is a SizedBox (or any other widget). By making your parameters height and width instead of a SizedBox, you're signaling that you may do something else with those values, like print them. Instead, you can just accept a Widget directly.

import "package:flutter/material.dart";

class MyList extends StatelessWidget {
  @override
  Widget build(BuildContext context) => ListView(
    children: const [
      Text("Row 1"),
      Text("Row 2"),
    ].separate(const SizedBox(height: 10, width: 10)),
    // ^ by passing in a `const` SizedBox, the same instance is always reused. 
  );
}

extension ChildSpace on List<Widget> {  
  /// Returns itself but with [separator] in between each element.
  /// 
  /// You could also make this parameter a [SizedBox], but Flutter likes to be Widget-agnostic. 
  List<Widget> separate(Widget separator) => [
    for (final Widget child in this) ...[
      child,
      separator, 
    ]
  ]..removeLast();
}

Regarding generalization, you have to ask yourself, "Is it wrong to call ChildSpace.separate with a non-constant value?" The answer, to me, is no because height and width are parameters and therefore not known in advance. You could, for example, compute these values with, say, a LayoutBuilder first and then pass them to the function. If they were truly known constants, you could use a const SizedBox directly in the for loop and have no parameters.

Because you want to use const objects, you can build the SizedBox yourself and pass it directly to the non-const function. But that doesn't (and shouldn't) make const usage incompatible with non-const usage.

alrajdev commented 2 years ago

@Levi-Lesches Thank you for your time.

Your solution is ok for the code I used in the issue. But it was only part of the code I really use. In the extension I use, the setSpace function is actually setSpace({double? height, double? width, double? start, double? end}), the start and end is used to add SizedBox as the first or last child for spaces at the beginning and end.

And also I use two custom widgets:

  1. ChildSpace(double space) to adjust the default height or width used in setSpace().
  2. NoSpace() to not insert SizedBox in the list.
    [
    const Text("A"),
    Text(b),
    ChildSpace(-3), // decrease 3 from default height
    const Text("C"),
    ChildSpace(3), // increase 3 to default height
    Text(d),
    NoSpace(),  // don't insert SizedBox here, so no space
    Text("E"),
    ].setSpace(height: 10, start: 20, end: 30),

So I cannot use SizedBox as the argument here.

And also with the ChildSpace widget, I add or subtract the height or width with the ChildSpace()'s space argument, and this is why I requested for const return type, so that I can also use the value of a mathematical function as a const parameter.

extension _ChildrenSpaceSum on double {
  /// returns 0.0 if addition results to negative
  double positiveOrZero(double space) {
    final result = add(space);
    return result >= 0.0 ? result : 0.0;
  }

  double add(double other) => this + other;
}
SizedBox(
   height: height?.add(start),
   width: width?.add(start),
)

And the whole point of asking for const is not for this function only, but for every function where we build non-changeable Widgets.

But instead of using const for build optimization if flutter used some new keyword I think this problems won't be a problem, like norebuild or something.

for (final child in sublist(1)) {
   spacedChildren.addAll([
     // add space between every child
     norebuild SizedBox(
       height: height,
       width: width,
     ),
     child,
   ]);
 }

I don't know how flutter keeps the const Widget and resuses, But since the SizedBox already has the height and width, it can rebuild the child correctly right? so the height and width doesn't need to be const if a new keyword is used for the build optimization instead of const.