Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

hope for do expression #132

Open
xialvjun opened this issue Dec 13, 2018 · 11 comments
Open

hope for do expression #132

xialvjun opened this issue Dec 13, 2018 · 11 comments
Labels
request Requests to resolve a particular developer problem

Comments

@xialvjun
Copy link

I think do expression is very useful in flutter's html-like code.

And do expression shouldn't be just anonymous function calling even though their code length are very near, because using anonymous function calling, the generator status can not pass down.

please have a look at tc39/proposal-do-expressions#37

@yjbanov
Copy link

yjbanov commented Dec 13, 2018

I don't read JSX very well, but semantically it looks similar to the argument blocks idea.

Could you please provide a full Flutter code sample showing how do expressions would work?

@munificent
Copy link
Member

munificent commented Dec 13, 2018

Could you please provide a full Flutter code sample showing how do expressions would work?

A do expression is basically just a way to put a block of statements in a context where an expression is expected:

print(do {
  var a = 1;
  var b = 2;
  a + b
}); // Prints "3".

It's like an IIFE, except that IIFE's don't compose correctly with async, sync*, break, and continue.

Interestingly, we are considering adding a construct like this as part of the intermediate language that Dart is compiled to. See: #127 (comment)

It would be handy to have as a meta-syntax for specifying language features that are syntactic sugar. I'm not sure how useful it would be as a user-visible syntax feature.

@kasperpeulen
Copy link

kasperpeulen commented Dec 14, 2018

This is one usecase in JSX:

return (
  <nav>
    <Home />
    {
      do {
        if (loggedIn) {
          <LogoutButton />
        } else {
          <LoginButton />
        }
      }
    }
  </nav>
)

Without a do expression, you either have to write:

  • a ternary (which may be less readable)
  • an IIFE (which is more ugly/unusual)
  • a function (which may give unesssary abstraction/indirection)
  • or factor it out above (defeats the goal to have a single nested expression tree that you can read from top-to-bottom and outside-in)

For Flutter, if you go with the last option, you probably would write something like this:

var button;
if (loggedIn) {
  button = LogoutButton();
} else {
  button = LoginButton();
}
return Nav(
  children: [
    Home(),
    button,
  ]
);

The control flow collection proposal tries to solve this in the following way:

return Nav(
  children: [
    Home(),
    if (loggedIn)
      LogoutButton()
    else 
      LoginButton(),
  ]
);

If Dart would adopt do expressions, it would look like this:

return Nav(
  children: [
    Home(),
    do {
      if (loggedIn) {
        LogoutButton();
      } else { 
        LoginButton();
      }
    },
  ]
);

The Flow Collection Proposal is shorter, and also allows to add "Nothing" to the collection, when there is no else.

However do expression can also be used in other contexts, for example, if you want type inference, no- nullability and immmutability in the following code:

  factory Car(Position start, bool horizontal, int length) {
    var end;
    if (horizontal) {
      end = start + new Position(length - 1, 0);
    } else {
      end = start + new Position(0, length - 1);
    }

    var positions;
    if (length == 2) {
      positions = <Position>[start, end];
    } else {
      if (horizontal) {
        positions = <Position>[start, start + new Position(1, 0), end];
      } else {
        positions = <Position>[start, start + new Position(0, 1), end];
      }
    }
    return new Car._(start, end, horizontal, length, positions);
  }

With do expression it can be written like:

  factory Car(Position start, bool horizontal, int length) {
    final end = do {
      if (horizontal) {
        start + new Position(length - 1, 0);
      } else {
        start + new Position(0, length - 1);
      }
    }

    final positions = do {
      if (length == 2) {
        <Position>[start, end];
      } else {
        if (horizontal) {
          <Position>[start, start + new Position(1, 0), end];
        } else {
          <Position>[start, start + new Position(0, 1), end];
        }
      }
    }
    return new Car._(start, end, horizontal, length, positions);
  }

@lrhn lrhn added the request Requests to resolve a particular developer problem label Dec 14, 2018
@lrhn
Copy link
Member

lrhn commented Dec 14, 2018

There are complications to allowing statements with expressions. Currently, the only control flow an expression can do is throwing (which is also the only non-local control flow).
Local control flow, like break/continue/return, can only happen at the statement level (and await is an exception because it is a kind of control flow, and local, and allowed in expressions, which is one of the reasons compiling async functions is hard).

If we allow expression-blocks like do { statements; expressionStatement; }, it's not clear that we would allow them to do local control flow. No var x = do { if (something) return 42; "didn't return"; } because that would potentially complicate some things (and throw readability out the window).

@kasperpeulen
Copy link

@lrhn This question is also discussed here:

tc39/proposal-do-expressions#30

@munificent
Copy link
Member

return Nav(
  children: [
    Home(),
    do {
      if (loggedIn) {
        LogoutButton();
      } else { 
        LoginButton();
      }
    },
  ]
);

For that to work, there would have to be some implicit magic that the value usually discarded by a statement expression would somehow propagate out to the surrounding context. How would the language know that you want to keep the values returned by LogoutButton() and LoginButton(), but ignore the value returned by, say, print:

return Nav(
  children: [
    Home(),
    do {
      print("Just some debug code.")
      if (loggedIn) {
        LogoutButton();
      } else { 
        LoginButton();
      }
    },
  ]
);

Also, what happens if the do expression tries to emit multiple values, but is used in a context where only one is expected? What would this do:

var wat = do {
  LogoutButton();
  LoginButton();
};

@xialvjun
Copy link
Author

xialvjun commented Dec 15, 2018

I don't know if flutter support concept of render-props-component. If it do, then do expression has use cases like:

return Nav(
  children: [
    Home(),
    do {
      // Auth is a RenderPropsWidget that its child should be a callback function, and pass a boolean login status to that function
      var loggedIn = yield Auth();
      if (loggedIn) {
        LogoutButton();
      } else { 
        LoginButton();
      }
    },
  ]
);

And we can leave out the keyword do. Many expression-first-classed languages like rust leave out do.

do is not important, expression-first is important.

@kasperpeulen
Copy link

kasperpeulen commented Dec 16, 2018

For that to work, there would have to be some implicit magic that the value usually discarded by a statement expression would somehow propagate out to the surrounding context.

I couldn’t find really good documentation, but I believe it works similar as in “expression orientated languages”, the last executed statement expression is value that the do expression evaluates to.

You can try out how it works here. It is included in the babel stage-1 preset.

@Zhuinden
Copy link

Zhuinden commented Jan 1, 2019

var end;
if (horizontal) {
  end = start + new Position(length - 1, 0);
} else {
  end = start + new Position(0, length - 1);
}

Interestingly, in Kotlin you can do

val end = if (horizontal) {
    start + new Position(length - 1, 0);
} else {
    start + new Position(0, length - 1);
}

or its more preferred variant

val end = when {
    horizontal -> start + new Position(length - 1, 0);
    else -> start + new Position(0, length - 1);
}

@mattrberry
Copy link

mattrberry commented May 8, 2023

Just to toss in my 2 cents, I think a feature like this would be particularly useful now that switch expressions exist and switchExpressionCase is only defined over expressions. To reference another language with pattern matching and block expressions, we may be able to look to Rust's block expressions

@caseycrogers
Copy link

caseycrogers commented Jun 5, 2023

Just adding to the "hey I'd like this" end of the conversation! As @mattrberry says above, this is especially useful for the new switch expressions-right now it's a bit ungainly that you might have to refactor your expression to a statement if you need a multi-statement body in your switch. I go into a lot more depth here:
#3117

FWIW, as others have already pointed out, you can already do this today with IIFEs (Immediately Invoked Function Expressions).
Even though IIFEs don't play nicely with break etc, I'm slowly coming around to them.
yield, break and continue are all not super common keywords and I try not to use the latter two too often because they can be hard to reason about. Using them from within an expression block (whether as a new first class language feature or just an IIFE) seems a bit fundamentally confusing. So if that's a reasonably niche context and a fundamentally confusing thing to do, maybe it's fine not to support it?

async/await seems like by far the most important one, and while it's a bit tedious the following seems to work:

Future<String> funcA(int n) async {
  return switch (n) {
    0 => () async {
        await someAsyncSideEffect();
        return 'zero';
      }(),
    _ => Future.value(n.toString()),
  };
}

And if you really want yield, you can compose it in the same way async composes. It's a bit kludgy, but I don't think catastrophically so?

All this said, I definitely wouldn't complain if a language native do block were added (regardless of the exact syntax used). I'd also especially like for if (...) {} else (...) {} to be valid as an expression-I've seen this a couple times in the examples above (in fact, it seems like the most popular reason for wanting a do block) and I honestly think this should be considered independently of a do block and as such maybe should be filed as a separate issue (maybe it already has been?).

I don't think I love the idea of a do block WITHOUT making if...else an expression. For example, if this is valid and evaluates to "bar":

var myString = do {
  if (false) {
    'foo';
  } else {
    'bar';
  }
}

but this is a compile error:

var myString = if (false) {
  'foo';
} else {
  'bar';
]

this is semantically coherent, but pretty counterintuitive and hard to grock imo.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
request Requests to resolve a particular developer problem
Projects
None yet
Development

No branches or pull requests

8 participants