Checking function parameters in Phobos

Simen Kjærås simen.kjaras at gmail.com
Mon Nov 25 00:51:50 PST 2013


On 2013-11-25 08:24, Meta wrote:
> - The function validated would probably be better named validate, since
> it actually performs validation and returns a validated type. The
> struct's name is fine.

Yeah, I was somewhat torn there, but I think you're right. Fixed.


> - I think it'd be better to change "static if (is(typeof(fn(value)) ==
> bool))" to "static if (is(typeof(fn(value)) : bool))", which rather than
> checking that the return type is exactly bool, it only checks that it's
> implicitly convertible to bool, AKA "truthy".

Even better - test if 'if (fn(value)) {}' compiles. Fixed.


> - It might be a good idea to have a version(AlwaysValidate) block in
> assumeValidated for people who don't care about code speed and want
> maximum safety, that would always run the validation functions. Also, it
> might be a good idea to mark assumeValidated @system, because it
> blatantly breaks the underlying assumptions being made in the first
> place. Code that wants to be rock-solid @safe will be restricted to
> using only validate. Or maybe that's going too far.

@safe is only for memory safety, which this is not. I agree it would be 
nice to mark assumeValidated as 'warning, may not do what it claims', 
but @safe is not really the correct indicator of that.


> - Validated doesn't work very well with reference types. The following
> fails:
>
> class CouldBeNull
> {
> }
>
> bool notNull(T)(T t)
> if (is(T == class))
> {
>      return t !is null;
> }
>
> //Error: cannot implicitly convert expression (this._value) of type
> inout(CouldBeNull) to f505.CouldBeNull
> void takesNonNull(Validated!(CouldBeNull, notNull) validatedT)
> {
> }

Yeah, found that. It's a bug in value(), which should return inout(T), 
not T. Fixed.


> - On the subject of reference types, I don't think Validated handles
> them quite correctly. This is a problem I ran into, and it's not an easy
> one. Assume for a second that there's a class FourtyTwo that *does* work
> with Validated:
>
>      class FortyTwo
>      {
>          int i = 42;
>      }
>
>      bool containsFortyTwo(FortyTwo ft)
>      {
>          return ft.i == 42;
>      }
>
>      void mutateFortyTwo(Validated!(FortyTwo, containsFortyTwo) fortyTwo)
>      {
>          fortyTwo.i = 43;
>      }
>
>      auto a = validated!containsFortyTwo(new FortyTwo());
>      auto b = a;
>      //Passes
>      assert(a.i == 42);
>      assert(b.i == 42);
>      mutateFortyTwo(a);
>      //Fails
>      assert(a.i == 43);
>      assert(b.i == 43);
>
> This is an extremely contrived example, but it illustrates the problem
> of using reference types with Validated. It gets even hairier if i
> itself were a reference type, like a slice:
>
>      void mutateCopiedValue(Validated!(FortyTwo, containsFortyTwo)
> fortyTwo)
>      {
>          //We're not out of the woods yet
>          int[] arr = fortyTwo.i;
>          arr[0] += 1;
>      }
>
>          //Continuing from previous example,
>          //except i is now an array
>      mutateCopiedValue(b);
>      assert(a.i[0] == 44);
>      assert(b.i[0] == 44);
>
> Obviously in this case you could just .dup i, but what if i were a class
> itself? It'd be extremely easy to accidentally invalidate every
> Validated!(FortyTwo, ...) in the program in a single swipe. It gets even
> worse if i were some class reference to which other, non-validated
> references existed. Changing those naked references would change i, and
> possibly invalidate it.

This is a known shortcoming for which I see no good workaround. It would 
be possible to use std.traits.hasAliasing to see which types can be 
safely .dup'ed and only allow those types, but this is not a solution I 
like.

I guess it could print a warning when used with unsafe types. If I were 
to do that, I would still want some way to turn that message off. Eh. 
Maybe there is no good solution.


What else is new?
- Better error messages for invalid constraints (testing if an int is
   null, a string is divisible by 3 or an array has a database
   connection, e.g.)
- Fixed a bug in opCast (I love that word - in Norwegian it [oppkast]
   means puke. ...anyways...) when converting to an incompatible wrapped
   value.

--
   Simen

-------------- next part --------------
module biotronic.utils;

import std.typetuple : TypeTuple, NoDuplicates, staticIndexOf;
import std.traits : Unqual, ParameterTypeTuple;

void staticEnforce(bool criteria, string msg)() {
    static if (!criteria) {
        pragma(msg, msg);
        static assert(false);
    }
}

void staticEnforce(bool criteria, string msg, string file, int line)() {
    staticEnforce!(criteria, file ~ "(" ~ line.stringof ~ "): Error: " ~ msg);
}

auto sum( R )( R range ) if ( isInputRange!R ) {
    ElementType!R tmp = 0;
    return reduce!( (a,b)=>a+b )( tmp, range );
}

template arrayToTuple( alias name ) {
    static if ( name.length ) {
        alias arrayToTuple = TypeTuple!( name[0], arrayToTuple!( name[1..$] ) );
    } else {
        alias arrayToTuple = TypeTuple!( );
    }
}

template Repeat( size_t n, T... ) {
    static if ( n ) {
        alias Repeat = TypeTuple!( T, Repeat!( n-1, T ) );
    } else {
        alias Repeat = TypeTuple!();
    }
}

template hasFloatBehavior( T ) {
    static if ( __traits( compiles, { T t; t = 1; return (t/2)*2 == t; } ) ) {
        enum hasFloatBehavior = { T t; t = 1; return (t/2)*2 == t; }();
    } else {
        enum hasFloatBehavior = false;
    }
} unittest {
    assert( hasFloatBehavior!float );
    assert( hasFloatBehavior!double );
    assert( hasFloatBehavior!real );
    assert( !hasFloatBehavior!int );
    assert( !hasFloatBehavior!char );
    assert( !hasFloatBehavior!string );
}

template hasNumericBehavior( T ) {
    template hasNumericBehaviorImpl( U... ) {
        static if ( U.length ) {
            enum hasNumericBehaviorImpl = is( Unqual!T == U[0] ) || hasNumericBehaviorImpl!( U[1..$] );
        } else {
            enum hasNumericBehaviorImpl = false;
        }
    }
    
    enum hasNumericBehavior = hasNumericBehaviorImpl!( byte, short, int, long, ubyte, ushort, uint, ulong, float, double, real );
} unittest {
    foreach ( Type; TypeTuple!( byte, short, int, long, ubyte, ushort, uint, ulong, float, double, real ) ) {
        assert( hasNumericBehavior!Type );
    }
    foreach ( Type; TypeTuple!( string, char, dchar, int[], void, void*) ) {
        assert( !hasNumericBehavior!Type );
    }
}

template StaticFilter(alias pred, T...) {
    static if (T.length == 0) {
        alias StaticFilter = TypeTuple!();
    } else static if (T.length == 1) {
        static if (pred!(T[0])) {
            alias StaticFilter = T;
        } else {
            alias StaticFilter = TypeTuple!();
        }
    } else {
        alias StaticFilter = TypeTuple!(
            StaticFilter!(pred, T[0..$/2]),
            StaticFilter!(pred, T[$/2..$]));
    }
}

struct CMP(T...){}

template sortPred(T...) if (T.length == 2) {
    static if ( TypeTuple!(T[0]).stringof < TypeTuple!(T[1]).stringof ) {
        enum sortPred = -1;
    } else static if ( TypeTuple!(T[0]).stringof > TypeTuple!(T[1]).stringof ) {
        enum sortPred = 1;
    } else {
        enum sortPred = 0;
    }
} unittest {
    assert( sortPred!(int, string) == -sortPred!( string, int ) );
}

template StaticSort(alias pred, T...) {
    static if (T.length == 0) {
        alias StaticSort = TypeTuple!();
    } else static if (T.length == 1) {
        alias StaticSort = T;
    } else {
        template lessPred(U...) {
            enum lessPred = pred!(T[0], U[0]) == 1;
        }
        template equalPred(U...) {
            enum equalPred = pred!(T[0], U[0]) == 0;
        }
        template morePred(U...) {
            enum morePred = pred!(T[0], U[0]) == -1;
        }
        
        
        alias eq = StaticFilter!(equalPred, T);
        alias less = StaticFilter!(lessPred, T);
        alias more = StaticFilter!(morePred, T);
        
        alias StaticSort = TypeTuple!(
            StaticSort!(pred, less),
            eq,
            StaticSort!(pred, more));
    }
} unittest {
    assert(is(StaticSort!(sortPred, int, string) == StaticSort!(sortPred, string, int)));
    assert(is(StaticSort!(sortPred, int, string) == StaticSort!(sortPred, string, int)));
    
    assert(is(CMP!(StaticSort!(sortPred, int, "waffles", string)) == CMP!(StaticSort!(sortPred, "waffles", string, int))));
}

template hasNoDuplicates( T... ) {
    enum hasNoDuplicates = is( CMP!T == CMP!(NoDuplicates!T) );
}

template isSorted( T... ) {
    enum isSorted = is( CMP!T == CMP!(StaticSort!( sortPred, T ) ) );
} unittest {
    assert( isSorted!() );
    assert( isSorted!int );
    assert( isSorted!(int, int) );
    assert( isSorted!(int, string) );
    assert( !isSorted!(string, int) );
}

template TypeEnum(T...) {
    template TypeEnumName(int n) {
        static if (n < T.length) {
            enum TypeEnumName = "_" ~ n.stringof ~ "," ~ TypeEnumName!(n+1);
        } else {
            enum TypeEnumName = "";
        }
    }
    mixin("enum TypeEnum {"~TypeEnumName!0~"}");
}
    
template ParameterTypeTupleOrVoid(T...) if (T.length == 1) {
    static if (is(ParameterTypeTuple!T)) {
        alias ParameterTypeTupleOrVoid = CMP!(ParameterTypeTuple!T);
    } else {
        alias ParameterTypeTupleOrVoid = CMP!void;
    }
}

template isType(T...) if (T.length == 1) {
    enum isType = is(T[0]);
}

template TypeSet(T...) {
    template superSetOf(U...) {
        static if (U.length == 0) {
            enum superSetOf = true;
        } else static if (U.length == 1) {
            enum superSetOf = staticIndexOf!(U, T) != -1;
        } else {
            enum superSetOf = superSetOf!(U[0..$/2]) && superSetOf!(U[$/2..$]);
        }
    }
    
    template strictSuperSetOf(U...) {
        enum strictSuperSetOf = superSetOf!U && !is(CMP!T == CMP!U);
    }
} unittest {
    assert(TypeSet!(int, string).superSetOf!(int));
    assert(TypeSet!(int, string).superSetOf!(int, string));
    assert(!TypeSet!(int, string).superSetOf!(float));
    assert(!TypeSet!(int, string).superSetOf!(float, int, string));
    assert(!TypeSet!(int, string).superSetOf!(float, int));
}
-------------- next part --------------
module biotronic.validation;

import std.conv : to;
import biotronic.utils;

version (unittest) {
    import std.exception : assertThrown, assertNotThrown;
}

version (D_Ddoc) {
/**
Encapsulates a validated value, the validation of which is enforced through $(LREF validate). $(BR)
The unadorned value is available through $(LREF value), and through alias this. $(BR)
The constraints can either throw on their own, or return a bool value of true if the constraint passed, false if it didn't. $(BR)

Example:
----
bool isPositive(int value) {
    return value >= 0;
}

void checkLessThan42(int value) {
    enforce(value < 42);
}

void foo(Validated!(int, isPositive) value) {
}

foo(13); // Refuses to compile.

Validated!(int, isPositive, checkLessThan42) x = validate!(isPositive, checkLessThan42)(14); // Would throw on invalid input
foo(x); // It works!
----

A validated value A whose constraints are a superset of those of another validated type B may be implicitly converted. The opposite is not possible.

Example:
----
alias A = Validated!(int, isPositive, checkLessThan42);
alias B = Validated!(int, isPostive);

A a = 13;
B b = a;

a = b; // Error
----

If the wrapped type is convertible, and the constraints match, a type conversion is performed.

Example:
----
Validated!(int, isPositive) a = validate!isPositive(4);

Validated!(long, isPositive) b = a;
----

**/
    struct Validated(T, Constraints) if (Constraints.length > 0 && hasNoDuplicates!Constraints) {
        /// The wrapped value.
        @property public T value() { return T.init; }
    }
}

template Validated(T, _Constraints...) if (_Constraints.length > 0 && !isSorted!_Constraints && hasNoDuplicates!_Constraints) {
    alias Validated!(T, StaticSort!(sortPred, _Constraints)) Validated;
}

struct Validated(T, _Constraints...) if (_Constraints.length > 0 && isSorted!_Constraints && hasNoDuplicates!_Constraints) {
    alias _Constraints constraints;
    
    private T _value;
    @property inout
    public inout(T) value() {
        return _value;
    }
    alias value this;
    
    @disable this();
    
    /+
    debug {
        this(int line = __LINE__, string file = __FILE__)(T other) {
            alias create = validate!constraints;
            this = create!(T, line, file)(other);
        }
    } else {
        this(T other) {
            this = validate!constraints(other);
        }
    }
    +/
    
    this(U)(U other) if (isValidated!U && TypeSet!(U.constraints).superSetOf!(constraints) ) {
        _value = other._value;
    }
    
    typeof(this) opAssign(U)(U other) if (isValidated!U && TypeSet!(U.constraints).superSetOf!(constraints) && is(typeof(_value = other._value))) {
        _value = other._value;
        return this;
    }
    
    inout(U) opCast(U)() inout if (isValidated!U && TypeSet!(constraints).superSetOf!(U.constraints) && is(typeof(other._value = cast(typeof(other._value))_value))) {
        U result = void;
        result._value = cast(typeof(other._value))_value;
        return result;
    }
    
    inout(U) opCast(U)() inout if (is(T : U)) {
        return value;
    }
}

template isValidated(T...) if (T.length == 1) {
    static if (is(typeof(T))) {
        enum isValidated = isValidated!(typeof(T));
    } else {
        enum isValidated = is(T[0] == Validated!U, U...);
    }
} unittest {
    assert(isValidated!(Validated!(int, isPositive)));
    assert(isValidated!(validate!(isPositive)(4)));
    assert(!isValidated!string);
    assert(!isValidated!"foo");
}

/**
validate checks that the value passes all constraints, and returns a $(LREF Validated).

Example:
----
void foo(Validated!(int, isPositive) value) {
}

auto a = validate!isPositive(4);
foo(a);
----

Multiple constraints may be passed to validate.

Example:
----
auto b = validate!(isPositive, checkLessThan42)(54); // Will throw at runtime.
----
**/
template validate(Constraints...) if (Constraints.length > 0) {
    auto validateImpl(string loc, T)(T value) {
        import std.exception : enforce;
        import std.typetuple : TypeTuple;
        
        foreach (fn; Constraints) {
            staticEnforce!(is(typeof(fn(value))), loc ~ "Invalid constraint " ~ TypeTuple!(fn).stringof[6..$-1] ~ " for value of type " ~ T.stringof);
            static if (is(typeof({if (fn(value)){}}))) {
                enforce(fn(value), loc ~ "Validation failed for value (" ~ value.to!string ~ "). Constraint: " ~ TypeTuple!(fn).stringof[6..$-1]);
            }
            fn(value);
        }
        
        static if (isValidated!T) {
            Validated!(typeof(T._value), NoDuplicates!(Constraints, T.constraints)) result = void;
        } else {
            Validated!(T, Constraints) result = void;
        }
        result._value = value;
        return result;
    }
    debug {
        auto validate(T, int line = __LINE__, string file = __FILE__)(T value) {
            return validateImpl!(file ~ "(" ~ line.to!string ~ "): ")(value);
        }
    } else {
        auto validate(T)(T value) {
            return validateImpl!""(value);
        }
    }
} unittest {
    assertNotThrown(validate!(isPositive)(3));
    assertThrown(validate!(isPositive)(-4));
}

/**
assumeValidated does not run any checks on the passed value, and assumes that the programmer has done so himself. This is useful when checks may be prohibitively expensive or in inner loops where maximum speed is required.

Example:
----
auto a = assumeValidated!isPositive(-4);
----
**/
template assumeValidated(Constraints...) if (Constraints.length > 0) {
    auto assumeValidated(T)(T value) {
        version (alwaysValidate) {
            return validate!Constraints(value);
        } else {
            Validated!(T, Constraints) result = void;
            result._value = value;
            return result;
        }
    }
} unittest {
    assertNotThrown(assumeValidated!isPositive(-2));
}

version (unittest) {
    import std.exception : enforce;
    bool isPositive(int value) {
        return value >= 0;
    }
    void checkLessThan42(int value) {
        enforce(value < 42);
    }
    void checkString(string value) {
    }
    bool notNull(int* p) {
        return p !is null;
    }
}

unittest {
    void test1(int a) {}
    void test2(Validated!(int, isPositive)) {}
    void test3(Validated!(int, isPositive, checkLessThan42)) {}
    
    Validated!(int, isPositive, checkLessThan42) a = void;
    Validated!(int, isPositive) b = void;
    Validated!(long, isPositive) r = a;
    
    // Bug 11601
    pragma(msg, "Please ignore this warning:");
    assert(!__traits(compiles, {Validated!(int, checkString) s = validate!checkString(3);}));
    
    a = validate!(checkLessThan42, isPositive)(3);
    b = a;
    a = validate!(checkLessThan42)(b);
    
    test1(b);
    test1(a);
    test3(a);
    assert(!__traits(compiles, test2(3)));
    assert(!__traits(compiles, test3(b)));
    
    
    Validated!(int*, notNull) n = validate!notNull(new int);
}

void main() {
}


More information about the Digitalmars-d mailing list