[erlang-questions] Must and May convention

Fred Hebert <>
Fri Sep 29 14:53:38 CEST 2017


On 09/29, Richard A. O'Keefe wrote:
>On 29/09/17 4:33 AM, Fred Hebert wrote:
>
>>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}
>
>How is this different from 'let'?
>
>There was a very old proposal for Erlang that
>variables introduced between 'begin' ... 'end'
>brackets should be local, so that this would be
>    begin
>        {ok,Width}  = map:fetch(Opts, width),
>        {ok,Height} = map:fetch(Opts, height),
>        {ok, Width*Height}
>    end
>If memory serves me, this was proposed about 25 years
>ago, at about the same time that 'cond' was.
>

The distinction is in the control flow, rather than the scoping. With 
the `with' construct (and the reason why I compared it with monadic 
approaches), any value that represents an error returns and aborts the 
computation.

The construct is rather equivalent to:

    case map:fetch(Opts, width) of
        {ok, Width} ->
            case map:fetch(Opts, height) of
                {ok, Height} ->
                    {ok, Width*Height};
                {error, T} ->
                    {error, T}
            end;
        {error, T} ->
            {error, T}
    end.

The idea being that repeated conditionals based on the type of return 
(`{ok, Val} | {error, Term}`) could be abstracted over with a language 
construct.

- The pipe (|>) can be the simplest one where a value is directly passed
- the 'maybe' approach can be a branching based on `{ok,T}` or `{error, 
  T}` that picks whether to keep calling the waiting functions or 
  whether to return directly

Variants could be imagined based on a requirement such as "connection is 
active", or "file has not reached EOF", or others. I don't know that 
they would be practical, but that's why I mentioned in earlier posts 
that the monadic approach (bind + return with say Haskell's do notation) 
could be an interesting model.

>>
>>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)]])
>
>Or with
>    (fun ({ok,Width}, {ok,Height}) -> {ok,Width*Height} end
>    )(map:fetch(Opts, width), map:fetch(Opts, height))
>

Right. There's multiple equivalent forms. Yours right there is a bit 
tricky because it's hard to make it "look nice" when you have, say, 15 
operations to queue up. In fact for Erlang's validation in such cases, 
the pattern I see the most often is just iterating over a list with many 
clauses. See the SSL validation code for example:

https://github.com/erlang/otp/blob/56f6f1829e1f3fd3752914b302276bc9bf490bbb/lib/ssl/src/ssl.erl#L1311-L1390

This single 'validation' function can then be applied to each element of 
a list of options; if one is bad, it has to throw or change the workflow 
and abort; in some cases, it may transform the value, and in most cases, 
it just stores it.

I'm curious to see if there isn't a better form for that approach.  
Currently the common forms would be:

- write a recursive function that handles everything (same as code 
  above)
- write a higher order function that separates control flow from the 
  validation logic (is_valid(OptName,Val) -> {store, Name, Val} | 
  {abort, Reason}) and is applied in a more generic manner
- nested case ... of constructs
- a try ... catch where all 'aborts' are considered equal

Under current Erlang, I'd favor the second option for very large 
sequences of operations; it's totally legit and regular functional 
programming, even though a bit cumbersome to write.

Maybe what I'm really after is more of a 'folding comprehension' where 
rather than mapping over the values of a list, an accumulator is being 
handled.

<|NewState || InitState ||
   {ok, Entry} <- List,
   {ok, SubEntry} <- op(Entry),
   abort_if_false(Entry),
   proceed_if_true(SubEntry) |>

Sounds like it could be general enough to do whatever you want after 
that. Under an abortive filter, Validation could then look like:

<| dict:store(K, V, D) || D <- dict:new() ||
   {ok, V} <- [dict:fetch(K, Opts)],
   is_valid(K, V) |>

And then there you go, computation complete.
Then again, the syntax isn't pretty, and the decision to pick 'abort' as 
a default rather than 'skip' is similarly arbitrary and impossible to 
validate.

Maybe we're just better off with the very long form stuff anyway. It's 
unambiguous and straightforward after all, and that makes for 
maintainable code.


More information about the erlang-questions mailing list