A little TIL regarding syntactic sugar

So this is just a dumb little post regarding syntax.

I’ve been pairing with a colleague who is new to Elixir lately and today we were briefly discussing how do/end is really syntactic sugar that essentially gets converted into a keyword list as [{:do, block}] and gets passed as the last argument to whatever is calling it. I was showing them how—while fairly useless—it even works with regular functions (not just macros) and I was creating modules with a single function in IEx to demonstrate. Afterwards this got me thinking if the following syntax would work:

foo = fn args ->
  args[:do]
end

foo.() do
  "bar"
end

Lo-and-behold it does!

Again, this is pretty useless in practice (and many of you are likely thinking “Sure, why wouldn’t that work?”) but it slightly expanded my own understanding of the ast/sugar ergonomics. Mostly I just thought it was cool and I’m home alone on a Friday night and I had to tell someone :upside_down_face:

If anyone has a better/different/(or even worse) way that they describe how do/end works, please feel free to share.

…and actually, are there practical uses of do/end for plain function functions I’m not thinking of?

6 Likes

Personally, I’d find a do block passed to a plain function really confusing because I’ve written Ruby for years where that’s the syntax (more or less) for passing an anonymous function that the recipient can call.

Other keywords like else would be confusing to any Elixir developer, since they evaluate earlier than expected:

foo = fn args ->
  args[:else]
end

foo.() do
  IO.puts("something")
else
  IO.puts("something else")
end
1 Like

Oh absolutely, which is why I said it was useless. But often when I say stuff like that people come back with an “aaaaccctually, in this one case…” but ya, I was pretty sure there was no good reason for it. I was just using it to illustrate to my coworker there is nothing particularly “special” with how do works.

I like the fact that you explored the “but wait, what if?” I never thought of exploring the option :sweat_smile:

2 Likes

I never said I didn’t think about it, I just couldn’t come up with anything worth sharing.

You could use it to make an over-engineered version of this:

foo = (
  bar = do_something()
  # ...
  bar
)
def cache(do: block), do: block

cache do
  bar = do_something()
  # ...
  bar
end

But foo = () is pretty useless as it is since variables leak so :person_shrugging:

This is very interesting, for some reason I always thought that having the do: block is special syntax that is only available in macros.

In some sense it is special as while the generated AST is just a keyword list, the tokenizer knows this is a special keyword.

1 Like

I meant “not special beyond what is already special about it.” I’m not the greatest communicator :sweat_smile:

I never said you did say that. So I am a bit confused by the reply. Guess we are misaligned in a world of non-native English writers :writing_hand:.

Oh yes, I completely misunderstood what you were saying! Sorry about that!

I’m not the greatest communicator

I thought about disagreeing with your statement until… :wink:

All is forgiven. You are so active in the community, I would not dare to be mad at you! Thanks for all your effort!

Fixed: “mad about you” is something else

2 Likes

Ohhhhh I re-read your comment and I just completely mis-read it. I’m definitely a bad reader. I read it as:

I like the fact that you explored the “but wait, what if?” AND never thought of exploring the option :sweat_smile:

and was very confused.

edit: “mad about you” is something else

:sweat_smile:

2 Likes

All of those different clauses that seem like language keywords that you can pass to the def macro, such as rescue, catch, else, after can be used in that implicit last keyword list argument. I’m pretty sure the parser limits to those atoms, which means you can’t pick arbitrary ones, however, knowing how that works could open up possibilities of cool macro features.

Aside: Yes, “Mad About You” was a fun 90’s sitcom :grin:

3 Likes

It is correct, the parser does seem to limit it, at least I did not succeed to use other keywords, even tough the macro definition does not seem to have such limitations. I didn’t go too deep down the rabbit hole. Probably with a lot of AST manipulation you might still be able to do it.

Is it good or bad, that I couldn’t do it? At the time I was (of course) very sad that I didn’t manage to do it and it did limit me in my creativity. I created my own DSL where I wanted to us a different keyword for the code block. I even wanted to have two code blocks (similar to a do-else- end block). On the other hand, I’m not sure whether it would have helped, because it might have lead me to create an unnatural, awkward, complex monster.

Are you talking about using any random word here? The “sugar” words are definitely reserved as they cannot be used as identifiers. Even though you can do some pretty wild stuff, Elixir doesn’t let you make up completely random syntax. For example, it only allows a fixed set of infix operators beyond the ones that are already in use. There are some other limitations that prevent you from too hog wild that I can’t remember off the top of my head.

There may be some way you could wrangle it but it would be super confusing since:

foo do
  true
bar
  false
end

Is normally just parsed as:

foo do
  true
  bar()
  false
end

You could treat everything between bar and end differently, but Elixir isn’t a whitespace significant language so it would just be really confusing. The formatter would always rewrite it to the latter form, for example.

Yes, that was exactly the initial plan to have something like

foo a: 1 bar
  # some code
end

Even though it does limit the DSL you can create from it. It does help to keep DSLs still somewhat consistent and I do think (in the meantime) that it’s a good thing :slight_smile: