On 22.11.2013 02:55, Simen Kjærås wrote:
On 22.11.2013 00:50, Meta wrote:
On Thursday, 21 November 2013 at 22:51:43 UTC, inout wrote:
What if you have more that just one validation, e.g. Positive and
LessThan42?
Is Positive!LessThan42!int the same type as LessThan42!Positive!int?
Implicitly convertible?

Allow multiple validation functions. Then a Validated type is only valid
if validationFunction1(val) && validationFunction2(val) &&...

Validated!(isPositive, lessThan42, int) validatedInt =
validate!(isPositive, lessThan42)(34);
//Do stuff with validatedInt

I believe inout's point was this, though:

   Validated!(isPositive, lessThan42, int) i = foo();

   Validated!(isPositive, int) n = i; // Fails.
   Validated!(lessThan42, isPositive, int) r = i; // Fails.

This is of course less than optimal.

If a type such as Validate is to be added to Phobos, these problems need
to be fixed first.


Or just pass a function that validates that the int is both positive and
less than 42, which would be much simpler.

I've created a version of Validated now that takes 1 or more constraints, and where a type whose constraints are a superset of another's, is implicitly convertible to that. Sadly, because of D's lack of certain implicit conversions, there are limits.

Attached is source (validation.d), and some utility functions that are necessary for it to compile (utils.d).

Is this worth working more on? Should it be in Phobos? Other critique?

Oh, sorry about those stupid questions, we have a term for that:

Detroy!

--
  Simen
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));
}
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 validated). $(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 = validated!(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 = validated!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 T value() {
        return _value;
    }
    alias value this;
    
    @disable this();
    
    /+
    debug {
        this(int line = __LINE__, string file = __FILE__)(T other) {
            alias create = validated!constraints;
            this = create!(T, line, file)(other);
        }
    } else {
        this(T other) {
            this = validated!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) ) {
        U result = void;
        result._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!(validated!(isPositive)(4)));
    assert(!isValidated!string);
    assert(!isValidated!"foo");
}

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

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

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

Multiple constraints may be passed to validated.

Example:
----
auto b = validated!(isPositive, checkLessThan42)(54); // Will throw at runtime.
----
**/
template validated(Constraints...) if (Constraints.length > 0) {
    auto validatedImpl(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(fn(value)) == bool)) {
                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 validated(T, int line = __LINE__, string file = __FILE__)(T value) 
{
            return validatedImpl!(file ~ "(" ~ line.to!string ~ "): ")(value);
        }
    } else {
        auto validated(T)(T value) {
            return validatedImpl!""(value);
        }
    }
} unittest {
    assertNotThrown(validated!(isPositive)(3));
    assertThrown(validated!(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) {
        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) {
    }
}

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;
    Validated!(int, checkString) s = validated!checkString(3);
    
    a = validated!(checkLessThan42, isPositive)(3);
    b = a;
    a = validated!(checkLessThan42)(b);
    
    test1(b);
    test1(a);
    test3(a);
    assert(!__traits(compiles, test2(3)));
    assert(!__traits(compiles, test3(b)));
}

void main() {
}

Reply via email to