[eeps] Multi-Parameter Typechecking BIFs

Richard O'Keefe ok@REDACTED
Mon Feb 23 01:39:51 CET 2009


On 20 Feb 2009, at 8:55 pm, mats cronqvist wrote:
>  I think I see what you're saying. Alas, your conclusion seems
>  backwards.

Obviously I don't think so.
>
>
>> Take again the very common case of "this argument is an
>> {M,F,A} triple".
>>
>> Right now, people write out, every time,
>>
>> 	f(...{M,F,A}...)
>> 	when is_atom(M), is_atom(F), is_integer(A), A >= 0
>> 	-> ...
>>
>> Writing this as
>>
>> 	f(...{M::atom,F::atom,A::integer}...)
>> 	when A >= 0
>> 	-> ...
>>
>> (A) Makes it much much harder to *see* the triple.
>
>  Marginally harder, I'd say. And with syntax haglighting on it would
>  be a non-issue.

"Oops there is a problem here,
  so let us imagine that it has been solved."

What syntax highlighting precisely?
How are you going to retrofit it into the editor I use?
How is this going to help when I print the module on
a black-and-white printer?
I have come to regard an appeal to syntax highlighting
as an admission of failure.

To me it makes the triple *much* harder to see.
One reason you may have thought otherwise is that I
gave you ample warning that a triple was coming.
But when you are reading someone else's code and DON'T
know to expect a triple, it's quite different.

>  But the problem with the current when syntax is exactly that; that
>  you have to split all the conditions in pieces (the first condition
>  being where in the argument the variable is bound.)

Not quite.  The current syntax does force you to separate
the *pattern match* from the *conditions*, but at least it
lets you write the *conditions* as one thing.  Putting ::test
in a pattern breaks what's logically one condition into lots
of little apparently unrelated pieces.
>

>> (C) Kicks abstraction in the teeth, beats it in the head with
>>    an iron bar, and steals its notecase.  Where does it say
>>    {M,F,A}::names_a_function?  This is a very very low level
>>    way of describing what you want.
>
>  You lost me here.

What's complicated about it?

There is an ABSTRACT DATA TYPE "function name".
This is a "high level" concept.  I want to be able
to name that concept, and to use that name whenever
I mean that concept.

Using abstract patterns:
	#function_name(M, F, A)
	when atom(M), atom(F), integer(A), A >= 0
	-> {M, F, A}.

Using a macro is not as good, because I cannot write
a single thing that means "THIS is a function name",
but I can at least write a single thing that means
"THESE form a function name".

{M, F, A} where M is an atom and F is an atom and A
is a non-negative integer  is a CONCRETE DATA TYPE
which can (and in Erlang does) serve as a representation
of the "function name" abstract data type.
The form {M::atom, F::atom, A::integer} doesn't mention
the high level concept anywhere.  Using {M,F,A} in one
place and ?function_name(M, F, A) in another does at
least mention the high level concept.

In the case of {M,F,A}, there's not much likelihood that
that will be changed.  But when it comes to ones own data
structures, there is every likelihood that there will be
more than one round of data structure design.

Let's take dates as the obvious example.
For many people the obvious representation is {Day, Month, Year}.
For others, the equally obvious representation is {Month, Day, Year}.
Eventually one realises that {Year,Month,Day} is better because you
can sort it directly.  So we go from
	#date(D,M,Y)
	when is_integer(D), 1 =< D, D =< 31,
	     is_integer(M), 1 =< M, M =< 12,
	     is_integer(Y), 1900 =< Y, Y =< 2200
	-> {D,M,Y}.
	#date(#date(D,M,Y)) -> #date(D,M,Y).

to	#date(D,M,Y) when ... -> {Y,M,D}.

Being able to make such a change is what abstract data types are
all about.  Well, without abstract patterns, we can't do that.
What about a macro?
	-define(date(D,M,Y),
	    is_integer(D), 1 =< D, D =< 31,
	    is_integer(M), 1 =< M, M =< 12,
	    is_integer(Y), 1900 =< Y, Y =< 2200).
Instead of writing
	f(..., #date(D,M,Y), ...) -> ...
we would have written
	f(..., {D,M,Y}, ...) when ?date(D, M, Y) -> ...

Not so good.  If we want to change the representation,
we have to hunt down each of those triples, and change
the order of its elements.  But at least we can find
the places that need changing by looking for '?date'.
And if we miss any, there's a good chance that the guard
will find them during testing.

But suppose instead this f example had been written as
	f(..., {D::integer,M::integer,Y::integer}, ...) -> ...
How are we to tell this low level description (a triple
holding three integers) from any other triple of three
integers?  (Such as a 3d point.)

If you fail to convert one of these, how will testing find it?
You *still* have to write the ?date(D, M, Y) test.  All that
putting type tests in the pattern has done is to make it
*harder* to write what you want (because you are now expected
to put the type tests in the pattern, every time, instead of
in the macro, once).

>> What we want, of course, is to say that the *whole* triple
>> satisfies a named condition.  And we can do that, right now,
>> with macros.
>>
>> 	-define(mod_func_arity(M, F, A),
>> 	    is_atom(M), is_atom(F), is_integer(A), A >= 0).
>>
>> 	f(...{M,F,A}...) when ?mod_func_arity(M,F,A) ->
>
>  But then I have to go looking in a header file somewhere to find the
> -define(mod_func_arity,...).
>  Yuck.

Yes you do.  But at least it is there to be found.
(Most modern text editors support some kind of tags facility,
which means that "to go looking" means the enormous burden of
putting the cursor on the macro call and pressing perhaps as
many as two keys.  As it happens, the text editor I use doesn't
support tags, so I'd have to type
	<ESC>! find-macro mod_func_arity<ESC>
Big deal.)

>> Guards are good.  I have had the misfortunate to have to read
>> "Prolog" code written in a dialect that allowed type tests in
>> patterns.  Never again.
>
>  Type checking in guards sucks. It splits the match condition into
>  pieces, makes it prone to stupid typo errors, and, worst of all,
>  presents a barrier to type checking that is high enough to make
>  people just skip it altogether.

I don't understand why you say "prone to .. typo errors".
Why is putting type tests in guards any more error-prone than
putting them into patterns?  Oddly enough, I would regard
type tests in patterns as creating a barrier to type checking
that would make people skip it altogether.

Thanks to macros (how can I live with myself for saying such a thing?)
the difficulty of putting type tests in guards is miniscule.  You
define your higher level checks (like "arity >= 0" or "month =< 12")
and NAME them and into their definitions you put the type tests and
then you never write another low level type test again.

>
>
>  mats
>




More information about the eeps mailing list