A programming convention

classic Classic list List threaded Threaded
11 messages Options
Reply | Threaded
Open this post in threaded view
|

A programming convention

Joe Williams-2

Here's an idea for a programming convention....

This convention has to do with the return value/exception problem.

The standard example is from dict.erl.

Suppose you search for a keyed value in a dictionary. Not finding a value
means one of precisely two things:

        1) A logical error in the program
        2) An expected behaviour of the application

In case 1) the application should terminate with an exception. In case 2)
the application should deal with the return value.

For this reason there are two functions which search for keys in dict.
They are

        dict:fetch(Key, Dict) -> Value | EXIT
        dict:find(Key, Dict) -> {ok,Value}| error

I can never remember which is which so I always have to consult the manual
page.

The programmer should use fetch if not finding a key is a logical programming
error otherwise find.

<<aside find violates the "principle of least surprise" and
should have returned {ok, V} |{error, Why}>>

An alternative would to *always* use find, and write

        {ok, Val} = dict:find(....)

In the case where a missing key represented should cause the program to crash.
The problems with this are twofold:

        a) You don't get a nice execution reason (badmatch)
        b) you need a lot of them in your code

i.e. you don't really want to write

        {ok, _} = io:format(...)
        {ok, _} = io:format(...)

        etc.

Now the convention. We always provide a *pair* of interface functions:

        lookup(Key, Dict)   -> Value | exit(Why)
        lookupQ(Key, Value) -> {ok, Value} | {error, Why}

The "Q" in the name signifies "Query" or "Question" in other words we do not
*know* if the function will work or not - this is the expected behavior
of the function.

Without the Q the function must work. The relationship is obvious

        lookup(Key, Dict) ->
            case (catch lookupQ(Key, Dict)) of
                {'EXIT', Why} ->
                        {error, Why};
                Other ->
                        {ok, Other}
            end.

We also say that functions with a Q at the end of the name *always*
return maybe() types << maybe() = {ok, Val} | {error, Why} >>

If we do all this we will *finally* be able to define an error in a program
as a "top level untrapped exit"

Comments welcome

...

        /Joe





Reply | Threaded
Open this post in threaded view
|

A programming convention

Ulf Wiger-4
On Tue, 11 Jun 2002, Joe Armstrong wrote:

>Without the Q the function must work. The relationship is
>obvious
>
> lookup(Key, Dict) ->
>    case (catch lookupQ(Key, Dict)) of
> {'EXIT', Why} ->
> {error, Why};
> Other ->
> {ok, Other}
>    end.

...Obvious, but (obviously) the opposite of the above. ;)

   lookupQ(Key, Dict) ->
      case (catch lookup(Key, Dict)) of
         {'EXIT', Why} ->
            {error, Why};
         Other ->
            {ok, Other}
      end.


>We also say that functions with a Q at the end of the name *always*
>return maybe() types << maybe() = {ok, Val} | {error, Why} >>
>
>If we do all this we will *finally* be able to define an error
>in a program as a "top level untrapped exit"
>
>Comments welcome


I don't have any objections. If there is a better alternative
than a 'Q' suffix, I can't think of it right now.

/Uffe
--
Ulf Wiger, Senior Specialist,
   / / /   Architecture & Design of Carrier-Class Software
  / / /    Strategic Product & System Management
 / / /     Ericsson Telecom AB, ATM Multiservice Networks



Reply | Threaded
Open this post in threaded view
|

A programming convention

Samuel Elliott
On 11/06, Ulf Wiger wrote:
| On Tue, 11 Jun 2002, Joe Armstrong wrote:
|
| >Without the Q the function must work. The relationship is
| >obvious
| >
| > lookup(Key, Dict) ->
| >    case (catch lookupQ(Key, Dict)) of
| > {'EXIT', Why} ->
| > {error, Why};
| > Other ->
| > {ok, Other}
| >    end.
|
| ...Obvious, but (obviously) the opposite of the above. ;)
|
|    lookupQ(Key, Dict) ->
|       case (catch lookup(Key, Dict)) of
|          {'EXIT', Why} ->
|             {error, Why};
|          Other ->
|             {ok, Other}
|       end.

I prefer Joe's solution: an exception should stay exceptional. The reason
for that the compiler designers may choose to make an exception handler
as cheap as possible ("zero cost") when no exception is raised, while
an exception may take a lot of time to be processed when it is raised.

Obviously this is not true in the current Erlang implementation, but may
become true if Erlang gets compiled to native code with table-driven
exceptions, whose setup cost is virtually zero and propagation time very
high.

  Sam



Reply | Threaded
Open this post in threaded view
|

A programming convention

Ulf Wiger-4
On Tue, 11 Jun 2002, Samuel Tardieu wrote:

>I prefer Joe's solution: an exception should stay exceptional.
>The reason for that the compiler designers may choose to make an
>exception handler as cheap as possible ("zero cost") when no
>exception is raised, while an exception may take a lot of time
>to be processed when it is raised.

The only thing I did was to change the names around so that the
semantics of the code were consistent with Joe's actual
suggestion.

|    lookupQ(Key, Dict) ->
|       case (catch lookup(Key, Dict)) of
|          {'EXIT', Why} ->
|             {error, Why};
|          Other ->
|             {ok, Other}
|       end.


I guess what you're proposing is that the following example would
be more appropriate:

   lookup(Key, Dict) ->
      case lookupQ(Key, Dict) of
         {error, Why} ->
            exit(Why);
         {ok, Value} ->
            Value
      end.

which, of course, could be used as a general pattern:

  lookup(Key, Dict) ->
     no_maybe(lookupQ(Key, Dict)).
  lookupQ(Key, Dict) ->
     %% assuming that dict is a Key-Value list
     case lists:keysearch(Key, 1, Dict) of
        {value, {_Key, Value}} ->
           {ok, Value};
        false ->
           {error, not_found}
     end.

   %% the general utility function, which should be inlined
   %%
   no_maybe({ok, Value}) -> Value;
   no_maybe({error, Why}) -> exit(Why).

/Uffe
--
Ulf Wiger, Senior Specialist,
   / / /   Architecture & Design of Carrier-Class Software
  / / /    Strategic Product & System Management
 / / /     Ericsson Telecom AB, ATM Multiservice Networks





Reply | Threaded
Open this post in threaded view
|

A programming convention

Richard Carlsson-4
In reply to this post by Joe Williams-2

On Tue, 11 Jun 2002, Joe Armstrong wrote:

> find violates the "principle of least surprise" and
> should have returned {ok, V} |{error, Why}>>
> [...]
> Now the convention. We always provide a *pair* of interface functions:
>
> lookup(Key, Dict)   -> Value | exit(Why)
> lookupQ(Key, Value) -> {ok, Value} | {error, Why}

I don't think {error, Why} is a good idea in cases like this.
First, let me say that I really do advocate a standardised maybe()
type for Erlang programs. However I don't think it should be mixed
with error descriptors. If it _is_ an error, then let it remain an
exception. If it is expected behaviour, then the "Why" part is
not relevant: typically (if you program with the maybe() pattern),
there is exactly one reason for "error", and any other exceptions
that may be raised during execution are _real_ errors. Suppose for
instance that you have a bug in your dictionary implementation which
suddenly triggers - do you want this to be transformed into a term
{error, Why}? (Probably, the application will then just toss away the
Why part and continue as if the key was not present. Bad thing.)

I usually use:

        maybe() ::= {value, X} | none

which I think has a more neutral look-and-feel than "ok" and "error",
even though "value" is 3 chars longer than "ok". :-]

For instance, it makes it look less weird to store a maybe() type in
a record field or similar:

        X#foo{a = {value, 42}, b = none, c = lookupQ(Key, Dict)}

("ok" and "error" would definitely seem strange here.)

In dictionary-like functions, I myself generally use "lookup" for the
maybe()-version, and "get" for the exception-raising version.
A naming convention for indicating that a function returns a maybe()
type might or might not be a good thing; I don't feel so sure that it
applies to other cases than dictionary lookup. In many cases you
might want to write functions that do return a maybe() type, but
which don't really have (or need) an exception-raising version. Should
you still feel obliged to name the function "fooQ" although there will
never be a "foo"?

> lookup(Key, Dict) ->
>    case (catch lookupQ(Key, Dict)) of

As a final comment regarding efficiency: Yes, exceptions are supposed to
be exceptional, but not extremely so. Even if you change to zero-cost
setup (which I believe that we have in HiPE these days), it must still
remain feasible to use "throw" for nonlocal returns - being able to do
so is part of the programming model.

        /Richard


Richard Carlsson (richardc)   (This space intentionally left blank.)
E-mail: Richard.Carlsson WWW: http://www.csd.uu.se/~richardc/
 "Having users is like optimization: the wise course is to delay it."
   -- Paul Graham



Reply | Threaded
Open this post in threaded view
|

A programming convention

Joe Williams-2
On Tue, 11 Jun 2002, Richard Carlsson wrote:

>
> On Tue, 11 Jun 2002, Joe Armstrong wrote:
>
> > find violates the "principle of least surprise" and
> > should have returned {ok, V} |{error, Why}>>
> > [...]
> > Now the convention. We always provide a *pair* of interface functions:
> >
> > lookup(Key, Dict)   -> Value | exit(Why)
> > lookupQ(Key, Value) -> {ok, Value} | {error, Why}
>
> I don't think {error, Why} is a good idea in cases like this.
> First, let me say that I really do advocate a standardised maybe()
> type for Erlang programs.

Yes but there seems to be a case for a maybe() type and an expected/unexpected behaviour
type.

We seem to need three types to denote intentionality (what the programmer meant)

  maybe() ::= {value, X} | none

        For situations where I know that something might not have a value and I'll
make no attempt at handling the unexpected behaviour.

        might() ::= {ok, Value} | {error, Why}

        For situations where I know that something might not work and I may wish
to handle the unexpected behaviour. Example handling {error, enofile} in file:open

        must() ::= Value | EXIT(Why)

        I require the expected behaviour and want to crash if I cannot continue.

<< aside

   if a function is written thus:

   foo(123) ->
        case X of
          ... ->
                abc;
          ... ->
                exit(bla)
        end.

  How should I document its type, like this???

        +type foo(int()) -> abc | EXIT(bla)
>>

I think the *important* thing is that *everybody* uses the standard types
and NOT their own variants.

One misplaced {yes, X} | no (in the middle of code which otherwise returns
{value, V} | none) plays havoc with your programming style.

<flame>

For years I have found code that returns one of two values.

many small things like switches which  had a state (on|off)

colored lights (red|green)

sexes male | female

Please (a million times)

*change* your code to work with BOOLEANS and rename your function appropriately.

example:

        sex(Person) -> male | female

Should be changed to

        is_female(person) -> true | false.

etc.

If you no this you will be able to use all the nice generics in the standard libraries,
the alternative is zillions of small conversion routines.

  If you  *insist* on  data abstraction you  could always  use (local)
macros to achieve the same thing!

</flame>

       

       

       

> However I don't think it should be mixed
> with error descriptors. If it _is_ an error, then let it remain an
> exception.

I agree

If it is expected behaviour, then the "Why" part is
> not relevant: typically (if you program with the maybe() pattern),
> there is exactly one reason for "error", and any other exceptions
> that may be raised during execution are _real_ errors. Suppose for
> instance that you have a bug in your dictionary implementation which
> suddenly triggers - do you want this to be transformed into a term
> {error, Why}? (Probably, the application will then just toss away the
> Why part and continue as if the key was not present. Bad thing.)
>

Yup.


> I usually use:
>
> maybe() ::= {value, X} | none
>
> which I think has a more neutral look-and-feel than "ok" and "error",
> even though "value" is 3 chars longer than "ok". :-]
>
> For instance, it makes it look less weird to store a maybe() type in
> a record field or similar:
>
> X#foo{a = {value, 42}, b = none, c = lookupQ(Key, Dict)}
>
> ("ok" and "error" would definitely seem strange here.)
>
> In dictionary-like functions, I myself generally use "lookup" for the
> maybe()-version, and "get" for the exception-raising version.
> A naming convention for indicating that a function returns a maybe()
> type might or might not be a good thing; I don't feel so sure that it
> applies to other cases than dictionary lookup. In many cases you
> might want to write functions that do return a maybe() type, but
> which don't really have (or need) an exception-raising version. Should
> you still feel obliged to name the function "fooQ" although there will
> never be a "foo"?

maybe()

(At least I wouldn't have to look up the manual page all the time)


>
> > lookup(Key, Dict) ->
> >    case (catch lookupQ(Key, Dict)) of
>
> As a final comment regarding efficiency: Yes, exceptions are supposed to
> be exceptional, but not extremely so. Even if you change to zero-cost
> setup (which I believe that we have in HiPE these days), it must still
> remain feasible to use "throw" for nonlocal returns - being able to do
> so is part of the programming model.
>
> /Richard
>
>
> Richard Carlsson (richardc)   (This space intentionally left blank.)
> E-mail: Richard.Carlsson WWW: http://www.csd.uu.se/~richardc/
>  "Having users is like optimization: the wise course is to delay it."
>    -- Paul Graham
>
>


/Joe



Reply | Threaded
Open this post in threaded view
|

A programming convention

Thomas Lange
In reply to this post by Ulf Wiger-4

Why dthen not introducing a higher order function. After
all, we use a functional language ;0)

>    lookupQ(Key, Dict) ->
>       case (catch lookup(Key, Dict)) of
>          {'EXIT', Why} ->
>             {error, Why};
>          Other ->
>             {ok, Other}
>       end.

In the Erlang style:

query(F,Args) ->
  case (catch F(Args)) of
       {'EXIT',Why}
          {error,Why};
       Other ->
          {ok,Other}
  end.

in case of lookupQ(Key,Dict) you write:

query(lookup,[Key,Dict])

This makes is very easy to find the places in you code
where you use this design pattern. The Q as addition is
more add-hoc and not as clear from a program analysis
point of view.

Alternatively, in a more curried style, one could define
several functions and write:

query(lookup)(Key,Dict)

For this one needs about ten functions of the form:

query(F) ->
  fun(X1) ->
     case (catch F(X1)) of
       {'EXIT',Why}
          {error,Why};
       Other ->
          {ok,Other}
     end
  end.


query(F) ->
  fun(X1,X2) ->
     case (catch F(X1,X2)) of
       {'EXIT',Why}
          {error,Why};
       Other ->
          {ok,Other}
     end
  end.

etcetera

/Thomas

---
Thomas Arts
Ericsson


Reply | Threaded
Open this post in threaded view
|

A programming convention

Samuel Elliott
In reply to this post by Ulf Wiger-4
On 11/06, Ulf Wiger wrote:

| The only thing I did was to change the names around so that the
| semantics of the code were consistent with Joe's actual
|
[...]
|
| I guess what you're proposing is that the following example would
| be more appropriate:

Yes, of course, thanks, I read your change and incorrectly inferred
the original proposal :)


Reply | Threaded
Open this post in threaded view
|

A programming convention

Fredrik Linder-2
In reply to this post by Joe Williams-2
> Here's an idea for a programming convention....
[...]
> Now the convention. We always provide a *pair* of interface functions:
>
> lookup(Key, Dict)   -> Value | exit(Why)
> lookupQ(Key, Value) -> {ok, Value} | {error, Why}
>
> The "Q" in the name signifies "Query" or "Question" in other words we do
not

> *know* if the function will work or not - this is the expected behavior
> of the function.
>
> Without the Q the function must work. The relationship is obvious
>
> lookup(Key, Dict) ->
>     case (catch lookupQ(Key, Dict)) of
> {'EXIT', Why} ->
> {error, Why};
> Other ->
> {ok, Other}
>     end.
>
> We also say that functions with a Q at the end of the name *always*
> return maybe() types << maybe() = {ok, Val} | {error, Why} >>
>
> If we do all this we will *finally* be able to define an error in a
program
> as a "top level untrapped exit"
>
> Comments welcome

Personally I do not believe such a convention would work... There are just
too many non-disiplined programmers out there. (:-) Your <flame> comment
about retruning booleans really points that out.

However, I do like the wrapper function proposed by Ulf Wiger.




Reply | Threaded
Open this post in threaded view
|

A programming convention

Chris Pressey
In reply to this post by Thomas Lange
On Tue, 11 Jun 2002 11:34:57 +0200
Thomas Arts <thomas> wrote:

>
> Why dthen not introducing a higher order function. After
> all, we use a functional language ;0)
>
> >    lookupQ(Key, Dict) ->
> >       case (catch lookup(Key, Dict)) of
> >          {'EXIT', Why} ->
> >             {error, Why};
> >          Other ->
> >             {ok, Other}
> >       end.
>
> In the Erlang style:
>
> query(F,Args) ->
>   case (catch F(Args)) of
>        {'EXIT',Why}
>           {error,Why};
>        Other ->
>           {ok,Other}
>   end.

Since often in the case of failure one might just want to supply a
reasonable default value, I would even go so far as to propose

  attempt(F,Args,Default) ->
    case (catch F(Args)) of
         {'EXIT',_} -> Default;
         Other      -> Other
    end.

> This makes is very easy to find the places in you code
> where you use this design pattern. The Q as addition is
> more add-hoc and not as clear from a program analysis
> point of view.

Totally agreed.  To me there is not much sense establishing a convention,
if there is some stronger way to enforce it, like adding a language
feature (or in this case even just a higher-order function).

-Chris


Reply | Threaded
Open this post in threaded view
|

A programming convention

Thomas Lindgren-3

I think the Right Way is to collect these "coding patterns"
in an OTP library, just like 'lists'.

That way, they will really become a part of the programming
practice. (OTP defines the de facto practice, I'd say :-)

Best,
                                        Thomas

-----Original Message-----
From: owner-erlang-questions
[mailto:owner-erlang-questions]On Behalf Of Chris Pressey
Sent: den 13 juni 2002 00:53
To: erlang-questions
Subject: Re: A programming convention


On Tue, 11 Jun 2002 11:34:57 +0200
Thomas Arts <thomas> wrote:

>
> Why dthen not introducing a higher order function. After
> all, we use a functional language ;0)
>
> >    lookupQ(Key, Dict) ->
> >       case (catch lookup(Key, Dict)) of
> >          {'EXIT', Why} ->
> >             {error, Why};
> >          Other ->
> >             {ok, Other}
> >       end.
>
> In the Erlang style:
>
> query(F,Args) ->
>   case (catch F(Args)) of
>        {'EXIT',Why}
>           {error,Why};
>        Other ->
>           {ok,Other}
>   end.

Since often in the case of failure one might just want to supply a
reasonable default value, I would even go so far as to propose

  attempt(F,Args,Default) ->
    case (catch F(Args)) of
         {'EXIT',_} -> Default;
         Other      -> Other
    end.

> This makes is very easy to find the places in you code
> where you use this design pattern. The Q as addition is
> more add-hoc and not as clear from a program analysis
> point of view.

Totally agreed.  To me there is not much sense establishing a convention,
if there is some stronger way to enforce it, like adding a language
feature (or in this case even just a higher-order function).

-Chris