[erlang-questions] Must and May convention

Fred Hebert <>
Thu Sep 28 17:33:29 CEST 2017


On 09/28, zxq9 wrote:
>Syntactic sugar is syntactic sugar, and generally undesirable imo.
>In this case syntactic sugar over pipeline functions which seem to
>exist merely to bewilder those who have yet to encounter their
>specially sugary syntactic forms in lang X or Y just yet.
>

Unless you have so much syntactic sugar (or sugar so ill-developed) it 
cannot be expected that the average language user knows all of them, you 
have to assume some level of knowledge in your users about the language 
they use.

We should be careful about an approach advocating for the lowest common 
denominator in the name of least surprise; a strict obedience to that 
rules would mean C-like syntax and no pattern matching since those are 
probably some of the trickiest things a newcomer can meet in Erlang.

>It is nearly always more interesting and self-consistent to build
>constructs such as this from the basic tools of the language, IN the
>language, and keep that language as small as possible than to cough
>up new syntax for each type of thing. It is a particularly bad sign
>when a language continues to accumulate new features and syntax well
>beyond its first year in production, especially when they are nifty
>little "me, too!" sort of sugary cheats.
>
>What if, for example, you want to continue evaluation to the end of
>the steps specifically because of some side-effects desired instead
>of shortcutting? Building a dead-obvious pipeline that does just that
>and says so in the name is not as likely to confuse someone as
>introducing new syntax to cover that one case plus the original 
>shortcut
>one. Two syntaxes for two types of pipelines... but there are many more
>kinds of pipelines. So where do we stop?

The question underlined here is not whether a given abstraction (with or 
without syntax) is warranted or not, but whether the given abstraction 
is composable enough.

As an example, Elixir has added the 'With' syntax:

    opts = %{width: 10, height: 15}
    with {:ok, width} <- Map.fetch(opts, :width),
         {:ok, height} <- Map.fetch(opts, :height),
      do: {:ok, width * height}

This is now some kind of new fancy syntax. However, Erlang lets you do 
something similar to with with list comprehensions:

    Opts = dict:from_list([{width,10}, {heigth,15}]),
    hd([{ok, Width*Height}
        || {ok, Width} <- [dict:find(width, Opts)],
           {ok, Height} <- [dict:find(height, Opts)]])

Even though the basic building blocks provided by list comprehensions 
allow the flexibility to do the desired operation provided by the `with' 
block and have allowed to do so for longer than Elixir has even existed, 
it is nearly never seen in the wild, and I avoid writing it whenever 
possible.

Direct basic flexible syntax is not a given synonym of a superior 
approach. The intent behind the list comprehension code is lost because 
more often, list comprehensions are about lists, not hijacking fancy 
pattern branching to get nifty flow control.

The tradeoff is not on whether the basic formula is the best, but 
whether the developers can communicate their intent in an error-free 
manner with the least amount of confusion to readers. Sometimes, adding 
syntax can be worth it for very common patterns, because in most cases, 
they will express things better.

>
>"Oh, I know! We'll add a new rule that you can 'hook stuff into this'
>as it zips along!" Great. One more thing to try to teach and debug...
>when the original construct, if left in the form of functions, would have
>been as concise, as clear, and not have wasted anyone's time learning
>which non alphanumeric chars on their keyboard they need to be more
>closely aware of.
>

A shortcut is a good thing if it saves you time with little risk. As an 
example, the need to always return something in Erlang has yielded 
shortcuts like the following ones for the cases where users don't need a 
return value:

    - case Cond of true -> log(Message); _ -> ok end
    - Cond andalso log(Message)
    - [log(Message) || Cond]
    - maybe_log(Cond, Message)  % repeated in every module

Those are likely rare enough and isolated enough that they are not worth 
their own syntax. In fact, some of them are clean enough that they may 
be totally acceptable and will sooner or later permeate regular practice 
at the confusion of newcomers.

The interesting question related to this is to ask when is a shortuct 
common enough to become a desire path? See 
https://en.wikipedia.org/wiki/Desire_path

Maps weren't in the original language, but people had such a need for 
them that it needed to be added. Even though you could very well make do 
with records and dicts and trees, people knew there was a more 
convenient path through maps. And hence the previously very common:

    #record{dict := Dict} = Record,
    {ok, Val1} = dict:find(Key1, Dict),
    {ok, Val2} = dict:find(Key2, Dict),
    Val1 + Val2

became:

    #record{dict = #{Key1 := Val1, Key2 := Val2}} = Record,
    Val1 + Val2

The potential for errors has gone down, the expressivity went up, and 
user convenience and satisfaction likely went up as well, since a lot of 
code now uses maps everywhere.

It is a bit too easy to just say "you don't really need that, here's a 
workaround". What's interesting is figuring out where or when it does or 
does not apply, and how could it be made composable enough not to stand 
out like a sore thumb and be amenable to extension in the future if need 
be.

So the |> pipe operator is not really perfect in elixir:

- it is strict with regards to position of the arguments
- it obscures the original number of arguments to a function (hello 
  tracing)
- it may compose or nest funny in some cases or other constructs
- it is limited in what it can do (see my previous post in this thread)
- it brings the language down a tricky avenue since other similar 
  control flow constructs would need to add more special operators with 
  new semantics
- other language features may be equivalent to it already

Nevertheless, it is hailed as one of the best features of the language 
by newcomers. I don't think I'd want it under its existing form in 
Erlang, but the tradeoff was deemed worth it by the Elixir community.

The question, to me, is not whether a pipe operator (or a `with' block) 
is required at all, but rather that if we wanted to provide one, which 
way would be good enough to have all the positives and few or none of 
the negatives?

I am able to get along fine without it, but if working without a similar 
construct requires 40 posts in a mailing list to make sure other people 
get a solution or pattern they are comfortable with when the operator is 
*not* available, then maybe there's a need to look at ways to improve 
the current state of things.

Regards,
Fred.


More information about the erlang-questions mailing list