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