[erlang-questions] Must and May convention
Fred Hebert
mononcqc@REDACTED
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