[m-dev.] shims

Mark Brown mark at mercurylang.org
Sun Sep 14 12:51:44 AEST 2014


On Thu, Sep 11, 2014 at 5:26 PM, Peter Wang <novalazy at gmail.com> wrote:
> Hi,
>
> When a (standard) library predicate is deprecated, the user faces a
> choice: update code now and break compatibility with older versions of
> the library, or face breakage in the future.  We try to give a lot of
> time in which to make that choice.  In the meantime the compiler keeps
> nagging, and it's hard to know when to make the break anyway.

The nagging can be stopped with --no-warn-obsolete. But this isn't
really about the warnings; I think what you're asking for is the
ability to target different versions of the same library.

>
> Ideally you could update code immediately but maintain a fallback for
> older libraries.  I don't think there is a way to do it currently, so

It can be done with autoconf and/or the C pre-processor, but that has
several disadvantages in my view:
(1) it's a hassle,
(2) some programmers may see it as an invitation to write spaghetti code,
(3) errors aren't caught in the parts that are not used (in contrast
to trace goals).

> perhaps it is time for another pragma, e.g.
>
>     :- pragma shim(p/1, other.p/1).
>
>     p(X) :- ...
>
> This says that p is a shim for other.p.  The clauses of p are only used
> if other.p is unavailable (not visible in the current module).  If
> other.p *is* available then p just forwards to other.p, and the given
> clauses of p are not checked semantically so that compilation will not
> break due to calling an obsolete predicate that has been since removed.

This still suffers from (3), meaning that developers with access to
other.p are liable to break the clauses for p, unless they recompile
with each targeted library version.

Here's another approach that I think provides an interesting solution.
Namely, a top level construct of the following abstract form:

    if
        declarations
    then
        items
    else if
        declarations
    then
        items
    ...
    else
        items
    end if

These can be nested. The meaning is that we check if there are visible
items matching the declarations in the conditions. If so, the clauses
in the first matching then branch are included in the module,
otherwise the clauses in the else branch are included. Other
declarations are local to the branch they appear in. In any case, all
sections are fully statically checked. The then branch is checked with
the condition's declarations included, replacing any that were already
visible, while the else branch is checked without them. Predicates or
functions declared outside this construct can have clauses defined
inside it; if so, the clauses must be defined in both branches.

Example (choosing the first concrete syntax that comes to mind):

    :- import_module set.
    :- pred portable_singleton_set(T::in, set(T)::out) is det.
    :- if.
        :- pred singleton_set(T::in, set(T)::out) is det.
    :- then.
        portable_singleton_set(X, Set) :- singleton_set(X, Set).
    :- else_if.
        :- pred singleton_set(set(T)::out, T::in) is det.
    :- then
        portable_singleton_set(X, Set) :- singleton_set(Set, X).
    :- else.
        % If neither of those work it will be a compile time error: no
clauses for
        % portable_singleton_set/2, with the `:- else.' as the
location of the error.
    :- end_if.

This is much more verbose than Peter's suggestion, but aside from the
better checking it's also much more flexible because you can target
different libraries, not just different versions of the same library.
For example, if there is a module called fastlib that only some
developers have in their path, you might write:

    :- pred algorithm(int::in, int::out) is det,
    :- if.
        :- import_module fastlib.           % This can fail.
        :- pred fastlib.algorithm(int::in, int::out) is det.
    :- then.
        algorithm(X, Y) :- fastlib.algorithm(X, Y).
    :- else.
        algorithm(X, Y) :- slow_algorithm(X, Y).
    :- end_if.

Note that, even though fastlib is imported, we still have to declare
the predicates from it that are used. This enables the then branch to
be checked even if the import fails, and also allows a check that
fastlib itself is the right version.

Finally, it may be desirable to provide implementations of abstract types:

    :- import_module string.
    :- type index.
    :- pred next_char(string::in, index::in, index::out, char::uo) is semidet.
    :- if.
        :- type string.code_point_index.     % Does the string module
export this?
        :- pred string.index_next(string::in, code_point_index::in,
code_point_index::out) is semidet.
    :- then.
        :- type index == code_point_index.
        next_char(String, !Index, Char) :- index_next(String, !Index, Char).
    :- else.
        :- type index == int.
        :- if.
            :- pred string.index_next(string::in, int::in, int::out,
char::out) is semidet.
        :- then.
            next_char(String, !Index, Char) :- index_next(String, !Index, Char).
        :- else.
            % We can't handle Unicode.
            next_char(String, !Index, Char) :-
                string.index(String, !.Index, Char),
                !:Index = !.Index + 1.
        :- end_if
    :- end_if.

Comments?

Cheers,
Mark.



More information about the developers mailing list