[erlang-questions] Must and May convention

zxq9 zxq9@REDACTED
Thu Sep 28 14:01:24 CEST 2017


On 2017年09月28日 木曜日 13:36:30 Joe Armstrong wrote:
> On Thu, Sep 28, 2017 at 8:14 AM, zxq9 <zxq9@REDACTED> wrote:
> > On 2017年09月28日 木曜日 05:43:33 you wrote:
> >> I dont think its been mentioned, elixir does this with the ! postfix.
> >>
> >> {ok, Bin} | {error, Reason} = File:read("my_file")
> >>
> >> Bin | throw/1 = File:read!("my_file")
> >>
> >>
> >> Exactly as you said Mr. Armstrong, the former is more clear off the bat but the latter gives you a nice error (with the filename!).
> >>
> >> Which do I prefer?  It seems both are useful in certain cases, and one should not replace the other as the absolute truth. If
> >> an absolute truth were to be arrived at, then I would vote for the option to have both! A way to call any STDLIB function and have it return a tuple, or throw/exception.
> >
> > Elixir may do that, but I think adding a magical glyph to the syntax is almost universally a mistake. Not because this one case is right or wrong, but because the thought process underlying it causes languages to eventually suffocate themselves -- as you may have begun noticing... And no, adding a bang at the end of a function call is NOT the same as naming two separate functions, unless bangs are acceptable characters to unclude in function names (are they?).
> >
> > If they are, some fun names might result:
> >
> >   launch!the!missiles!(BadGuys)
> >
> >   clean_your_room!dammit!(Kids)
> >
> > If they are legal characters, though, then what you are referring to is a mere coding convention -- arguably of less utility that matching wrapped values VS receiving naked ones, which has a concrete runtime effect (and is understood by the type system).
> >
> > If they are not legal characters and are actually a part of the core language (separate from the type system language) then... that's something I think should be contemplated for a HUGE amount of time before dropping into the language (things like that should be argued for years on end before just being picked as some new idea -- unless it is a research language, then, meh).
> >
> > That said, the point I was making is not that we should have both because everything is all the same and all functions should be treated equally and we really really need more diversity in the way we handle function call returns. The point I was making is that some functions are CLEARLY PURE and that is just an innate part of their nature. Other functions have side effects and, systems using runtime prayers/monadic returns aside, there is just no way to make a side effecty function pure.
> >
> > On that note, in a practical sense, when we return {Reply, NewState} in a gen_server we are sort of doing what IO monads do for Haskell. There is NO GUARANTEE from the perspective of the called function that the Reply value will actually be returned to the caller. That is obviously the intent, but it is by no means clear. This property also makes it very convenient to hook such functions up to a property tester. On the other hand, the tuple returned itself can be viewed as a naked value.
> >
> > Returning naked values mandates purity or total crashability of the system. Nothing in between. Unless you want to play "confuse the new guy".
> >
> > When we have side effects, such as file operations, there is no difference between calling
> >
> >   {ok, Bin} = file:read_file(Path)
> >
> > and
> >
> >   Bin = file!:read_file(Path)
> >
> > That's a frivolous reason to add a glyph to the language. The assertion at the time of return already crashes on an error.
> 
> Actually I half this but would prefer Bin=file:read_file!(P) - the problem
> is that ! is a weak typographic character - so it's easy to miss, and ! means
> send so we might (or rather will) confuse beginners.
> 
> I guess we could say
> 
>      Bin = MUST file:read_file(Path)
> 
> Easy to implement and it stands out nicely so you can see it from a
> long way off.
> Loosely scanning many lines of code and seeing MUST would be nice,
> it could be used to signify which parts of the code must be correct.

I still don't see it as more clear to the eye than

  {ok, Bar} = foo()

vs

  Bar = MUST foo()

vs

  Bar = foo!()

The thing I like most about the wrapped value is that it applies to
type checking at static analysis time, runtime effects at runtime, is
visually obvious and yet succinct, doesn't add anything to the grammar
and allows the caller to pick how the value should be handled (crash or
handle the received error).

> I actually quite liked the Elixir pipe operator - so I changed the
> Erlang parser and messed with this a bit.
> 
> So now I can write
> 
> test2(F) ->
>     F  |> file:read_file()
>         |> ok()
>         |> tokenize()
>         |> parse().
> 
> Instead of
> 
> test2(F) ->
>     {ok, B} = file:read_file(),
>     T = tokenize(B),
>     parse(T).
> 
> Where:
> 
> ok({ok,B}) ->
>     B.
> 
> I can't make my mind up about this - the version with pipes is slightly longer
> but seems very readable to me. Trouble is, if it crashes due to a non existent
> file, I can't see which file it is ...

I've thought about this a bit myself, and decided that in the context of
the Erlang runtime and Erlang syntax, I don't like it. Perhaps if we come
up with an Erlang2 with a slightly different semantic base I might like it.

(I went over some of that here, actually.
https://stackoverflow.com/questions/34622869/function-chaining-in-erlang/34626317#34626317
Sometimes Erlang *seems* verbose but I seem to consistently wind up with
*much* shorter programs overall in Erlang than when I write an equivalent
one in Python for anything non-trivial. And Python isn't so bad in terms
of program length, typically.)

It is the same reason I strongly resist the syntax OOP people try to bring
with them in the form of arguing for parameterized modules: it confuses
the concept of functional composition by adding a new syntax for it.

For example:

  test2(F) ->
      F = parse(tokenize(ok(file:read_file()))).

This exhibits no real differences because there is no underlying rule
applied other than simple composition. For example, there is no additive
rule that mandates that every call is actually being passed through
a function that looks like:

  must({ok, Value}) ->
      Value.

This would change the picture a lot, and allow functions to always be
written just one way instead of needing two versions.

Adding syntax for mere convenience is BAD unless we just want to mess
with people's minds. It is a recreation of the C++ and Haskell problem:
with 10 equivalent syntaxes, how many people can be expected to remember
and parse all of them when reading?

-Craig



More information about the erlang-questions mailing list