I believe `when` with `=` is visually ambiguous and works different from how people would expect it to.
The way I read the result of this AST: def foo(%{x: x} when x > 1 = result), do: 1 {:def, [context: Elixir, import: Kernel], [ {:foo, [context: Elixir], [ {:when, [], [ {:%{}, [], [x: {:x, [], Elixir}]}, {:=, [], [ {:>, [context: Elixir, import: Kernel], [{:x, [], Elixir}, 1]}, {:result, [], Elixir} ]} ]} ]}, [do: 1] ]} It means: def foo(%{x: x} when (x > 1 = result)), do: 1 Instead of: def foo((%{x: x} when x > 1) = result), do: 1 Allen Madsen http://www.allenmadsen.com On Mon, Sep 24, 2018 at 10:13 AM Christopher Keele <christheke...@gmail.com> wrote: > This is a more formal write-up of the discussion started with José here > <https://elixirforum.com/t/internal-guards/16723>. Interested in all > feedback, potential use-cases, and syntactic edge-cases! > > > Proposal > > Allow `*when conditions...*` to be placed anywhere within the patterns > used in functions, cases, and other such match head constructs—in addition > to the suffix-only version we support today. > > I believe this can be done by the compiler today as a backwards-compatible > enhancement with no change to the parser. > > Synopsis > > I'm proposing we allow guard clauses anywhere within match patterns, as > well as following them, so that the AST for match heads containing guards > can be composed easily. > > To show rather than tell, assuming you have AST for the following: > > X = x when x > 0 > Y = y when y > 1 > > Both *X* and *Y* are valid, stand-alone match heads. However, they are > not also valid stand-alone match patterns: there is no easy way to combine > them to form a new 2-arity match. The naive approach would be: > > XY = (x when x > 0, y when y > 1) > > While this is not currently a valid match head, once can hoist any > internal guards into a valid form like: > > XY2 = (x, y) when x > 0 and y > 1 > > This proposal explores allowing *XY* as valid Elixir code and rewriting > it to *XY2* at compile time. > > > Terminology > > To talk about this precisely I'm going to appropriate some ETS terminology > to reference Elixir syntax constructs I don't have canonical names for. Let > me know if you are aware of the correct terms for these! > > - a *match specification* is defined as a *match head -> body* pair > - a *match head* is defined as a *match pattern* optionally followed by *guard > clauses* > - a *match pattern* is defined as a comma delimited series of expressions > allowed in match contexts > - a *guard clause* is defined as a *when conditions* expression > formulated of functions allowed in guard contexts > > Technically, what I'm proposing is to loosen the restriction that guard > clauses must follow match patterns, by allowing match heads themselves to > recursively be valid expressions in parameters lists. The compiler can > extract all guard clauses found within a match head, leaving only a valid > match pattern, and combine the extracted guards with any existing ones to > create a new valid match head with equivalent semantics. > > > Parsing > > Currently, both of these (and all other variations I can think of) are > already valid syntax to the parser: > > fn x when x > 0, y when y > 1 -> #... end > def name(x when x > 0, y when y > 1), do: #... > > Only the compiler keeps you from running a program with these internal > guards; it can be parsed into AST with the exact precedence we want without > a hitch. > > > Grammar > > The only ambiguity I can think of would be the following: > > fn x when x > 0, y when y > 1 when y < 3 -> #... end > > It is not clear where whether the intent was to create an 'or' multi-guard > around the entire parameters list, or to guard both the last parameter and > the parameters at large with two separate clauses. The parenthesis > conventionally always used in *def*s resolves the ambiguity. I am open to > ideas on how to handle this situation, though personally I envision a > compile-time warning and treating it as a multi-guard as this is most > consistent with the precedence of *when*. I don't see it coming up too > often in generated code. > > It is worth observing that while the following technically has the same > ambiguity: > > fn x when x > 0, y when y > 1 -> #... end > > however you decide to treat the guard after the second parameter, the > resulting guards post-rewrite will be semantically equivalent. > > > Rewriting > > Since everything up to this point is already valid, I suspect the rewrite > could be done in a single place > <https://github.com/elixir-lang/elixir/blob/4d2f4865fe44ac05476a6908b43c9783e5ae8064/lib/elixir/src/elixir_utils.erl#L42-L54> > in the compiler with no further changes to any other code. > > The algorithm I have in mind is to simply walk the AST outside-in, > removing each set of consecutive guards it finds, and exploding the > permutation of each sets' multi-guards out to create new trailing clauses, > then *and*ing all terms together in each new guard clause. In the most > common case that simply entails prefacing the trailing guards with any > interior guard, all *and*ed together. > > This approach is intentionally naive about what variables are referenced > in which guards where. Anything that produces a valid guard in the end can > fly, even if generated code produces them in odd or unexpected places > within the params list. > > Simple extraction: > > def name(%{foo: bar when is_integer(bar), fizz: buzz when is_integer(buzz > )}) > when bar + buzz > 100 > > def name(%{foo: bar, fizz: buzz}) > when (is_integer(bar) and is_integer(buzz)) and (bar + buzz > 100) > > > In order to handle mutli-guards correctly we'd have to get a little *n^m *but > I doubt even the most ambitious metaprogramming would ever need to generate > code like that. > > Matrix of multi-guards with oddly referenced variables: > > def name(x, y when y > z when z > x, z) > when is_integer(z) > when is_string(z) > > def name(x, y, z) > when y > z and is_integer(z) > when y > z and is_string(z) > when z > x and is_integer(z) > when z > x and is_string(z) > > > Thoughts > > The purpose of this is to make function heads more composable in > meta-programming. Initially it would be released with zero fanfare except > perhaps a footnote in the guard docs. However, I can see this ability > catching on with people coming from strongly-specified typed languages, > allowing them to qualify type expectations in parameter lists inline with > where variables are defined. > > I don't think that would be too problematic since I find the style to be > pretty readable and more importantly the code still easy to reason about. > However, this is definitely enabling an alternate way to write pretty basic > syntax, so if we were really against human beings rather than macros > employing doing this, the rewrite process could emit warnings from code not > marked as generated. > > I have no idea how the formatter should handle this syntax. Perhaps its > behaviour already suffices, since it's pretty good at deconstructing long > function heads? > > I'm interested if there are strong opinions about this one way or the > other on the mailing list, as well as if there are any implications I've > overlooked in my suggested implementation. > > Thanks for reading! > Chris K > > -- > You received this message because you are subscribed to the Google Groups > "elixir-lang-core" group. > To unsubscribe from this group and stop receiving emails from it, send an > email to elixir-lang-core+unsubscr...@googlegroups.com. > To view this discussion on the web visit > https://groups.google.com/d/msgid/elixir-lang-core/29c74e96-efc0-4254-bd34-eb1fc2c03968%40googlegroups.com > <https://groups.google.com/d/msgid/elixir-lang-core/29c74e96-efc0-4254-bd34-eb1fc2c03968%40googlegroups.com?utm_medium=email&utm_source=footer> > . > For more options, visit https://groups.google.com/d/optout. > -- You received this message because you are subscribed to the Google Groups "elixir-lang-core" group. To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-core+unsubscr...@googlegroups.com. To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-core/CAK-y3CtFAFyLUE3JoysePRsNWkvPxcVZ1dmXsyWeEeXN%3Dw40LQ%40mail.gmail.com. For more options, visit https://groups.google.com/d/optout.