On Thu, Jul 17, 2014 at 11:06:30PM +0000, bearophile via Digitalmars-d-learn 
wrote:
> Justin Whear:
> 
> >What benefits would accrue from adding this?  Static verification
> >that a structure implements the specified concepts?
> 
> Not just that, but also the other way around: static verification that
> a "Concept" is strictly sufficient for any instantiation of a specific
> template. This is what Haskell/Rust do.
[...]

Right now, I'm still on the fence about the feasibility of implementing
concepts in D (esp. given our current direction to stabilize the
language with what we have), but I do see especially the following
strong benefit:

        /*
         * Current implementation:
         */
        template isInputRange(R) {
                // ... check for existence & types of .empty, .front,
                // .popFront
        }

        auto myFilter(R)(R range)
                if (isInputRange!R)
        {
                ...
                if (range.length > 5) { // <---- uh oh
                        ...;
                }
                return result;
        }

        unittest
        {
                auto r = myFilter([1,2,3,4]);
                assert(r.equals(expectedResult));
                // N.B. error in myFilter's implementation not caught by
                // unittest because arrays just happen to have more
                // properties than pure input ranges.
        }

        /*
         * Concepts implementation:
         */
        concept InputRange(T) {
                bool empty;
                T front;
                void popFront();
        }

        auto myFilter(R : InputRange)(R range) // hypothetical syntax
        {
                ...
                if (range.length > 5) { // <--- compile error:
                                        // InputRange doesn't have
                                        // .length property
                        ...
                }
                return result;
        }

Currently, there is no easy way to test if your range function is making
assumptions outside of the range API, because while signature
constraints are powerful, they also don't tell the compiler what
assumptions are granted to the function body -- the compiler has no way
to sanitize the function body until the template is actually
instantiated. But if your code wrongly depended on array-specific
properties, and your unittests only run tests with arrays, then the
problem won't be caught... until 3 months later when you ship your
library, and users complain that the function doesn't compile when
handed a non-array input range that doesn't have .length.

Using concepts, however, the compiler knows exactly what properties the
incoming type is assumed to have, and can therefore catch mistakes like
the above when you reference a property that isn't part of the concept.

This is a contrived example, of course. But it's a bit frightening how
many times I've come across Phobos code that makes wrong assumptions
like this.

A proper implementation of concepts would also catch more subtle errors,
such as the following:

        /**
         * Checks that R is an input range according to the range API.
         */
        enum isInputRange(R) = is(typeof(R.empty) : bool) &&
                                is(typeof(R.front)) &&
                                is(typeof(R.popFront()));

        auto myFilter(R)(R range)
                if (isInputRange!R)
        {
                ...
                auto tmp = range.front; // <-- spot the bug
                ...
                range.popFront();
                tmp = range.front; // <-- spot the bug
                ...
                return result;
        }

What bug? I hear you say. Well, the problem is, input ranges as defined
by the isInputRange template above is under-specified. It merely asserts
that R.front has a type, but says nothing about whether the type is
assignable, or, indeed, whether it's '@property void front()', in which
case the first "spot the bug" line will fail to compile since you can't
instantiate a void type. And again, since sig constraints tell the
compiler nothing about what assumptions are granted to the function
body, such problems are left hidden until one day somebody comes along
and wants to use your function with an unusual input range.

Under a concepts implementation, you'd perhaps write something like
this:

        concept InputRange(T) {
                bool empty;
                T front;
                void popFront();
        }

The compiler will then reject both "spot the bug" lines in myFilter,
because, as written, the definition of InputRange says nothing about the
type T. Is it instantiable? Assignable? Since it's not specified, it's
illegal to attempt those operations in myFilter. This then forces us to
rewrite the definition of InputRange, perhaps something like this:

        concept InputRange(T : Assignable, Instantiable) {
                bool empty;
                T front;
                void popFront();
        }

        concept Assignable {
                void opAssign(typeof(this)); // tell compiler assignment is 
allowed
        }

So you see, this has forced us to be more precise about exactly what an
input range is, instead of the current imprecise definition which leads
to subtle, corner case bugs like std.algorithm not handling transient
ranges correctly, etc..


T

-- 
PNP = Plug 'N' Pray

Reply via email to