Take the 2-minute tour ×
Programmers Stack Exchange is a question and answer site for professional programmers interested in conceptual questions about software development. It's 100% free, no registration required.

Many languages like C++, C#, and Java allow you to create objects that represent simple types like integer or float. Using a class interface you can override operators and perform logic like checking if a value exceeds a business rule of 100.

I'm wondering if it's possible in some languages to define these rules as annotations or attributes of a variable/property.

For example, in C# you might write:

[Range(0,100)]
public int Price { get; set; }

Or maybe in C++ you could write:

int(0,100) x = 0;

I've never seen something like this done, but given how dependent we have become on data validation before storage. It's strange that this feature hasn't been added to languages.

Can you give example of languages where this is possible?

share|improve this question
14  
Isn't Ada something like this? –  zxcdw May 21 '13 at 17:08
2  
@zxcdw: Yes, Ada was the first language (as I know) which has built in support for such "types". Named constrained data types. –  m0nhawk May 21 '13 at 17:10
4  
All dependently typed languages would have this ability. It's intrinsic to the type system en.wikipedia.org/wiki/Dependent_type realistically though you could create a custom type of this nature in any ML as well, in these languages a type is defined as data Bool = True | False and for what you want you could say data Cents = 0 | 1 | 2 | ... have a look at "Algebraic Data Types" (which should be more properly named hindley-milner types but people confuse that with type inference annoyingly) en.wikipedia.org/wiki/Algebraic_data_type –  Jimmy Hoffa May 21 '13 at 17:14
2  
Given how the languages you name handle integer over- and underflow, such a range restriction on its own wouldn't be worth much if you keep the silent over/underflow. –  delnan May 21 '13 at 17:27
9  
@StevenBurnap: Types do not require OO. There is a type keyword in Pascal after all. Object orientation is more of a design pattern than a "atomar" property of programming languages. –  wirrbel May 21 '13 at 17:58
show 9 more comments

6 Answers

up vote 26 down vote accepted

Pascal had subrange types, i.e. decreasing the number of numbers that fit into a variable.

  TYPE name = val_min .. val_max;

Ada also has a notion of ranges: http://en.wikibooks.org/wiki/Ada_Programming/Types/range

From Wikipedia....

type Day_type   is range    1 ..   31;
type Month_type is range    1 ..   12;
type Year_type  is range 1800 .. 2100;
type Hours is mod 24;
type Weekday is (Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday); 

can also do

subtype Weekend is  Weekday (Saturday..Sunday);
subtype WorkDay is  Weekday (Monday..Friday);

And here's where it gets cool

year : Year_type := Year_type`First -- 1800 in this case...... 

C does not have a strict subrange type, but there are ways to mimic one (at least limited) by using bitfields to minimize the number of bits used. struct {int a : 10;} my_subrange_var;}. This can work as an upper bound for variable content (in general I would say: don't use bitfields for this, this is just to proof a point).

A lot of solutions for arbitrary-length integer types in other languages rather happen on the library-level, I.e. C++ allows for template based solutions.

There are languages that allow for monitoring of variable states and connecting assertions to it. For example in Clojurescript

(defn mytest 
   [new-val] 
   (and (< new-val 10)
        (<= 0 new-val)))

(def A (atom 0 :validator mytest))

The function mytest is called when a has changed (via reset! or swap!) checks whether conditions are met. This could be an example for implementing subrange behaviour in late-binding languages (see http://blog.fogus.me/2011/09/23/clojurescript-watchers-and-validators/ ).

share|improve this answer
2  
If you would add a detail about dependent types as well would be nice, this problem is the entire purpose and reason for dependent typing, seems it should at least be mentioned (even if it is esoteric) –  Jimmy Hoffa May 21 '13 at 18:31
    
While I have some understanding of dependent types and inductive reasoning / milner-type inference. I have little practice with those. If you want to add information to my answer feel free to edit it. I was going to add something about the Peano Axioms and number types in math by inductive definition but a nice ML data example might be more worthwhile maybe. –  wirrbel May 21 '13 at 20:03
    
you could kludge a range type in C using enum –  John Cartwright May 23 '13 at 5:40
1  
enum is afaik of type int or unsigned int (I think it is compiler specific) and not bound-checked. –  wirrbel May 23 '13 at 5:48
add comment

Ranges are a special case of invariants. From Wikipedia:

An invariant is a condition that can be relied upon to be true during execution of a program.

A range [a, b] can be declared as a variable x of type Integer with the invariants x >= a and x <= b.

Therefore Ada or Pascal subrange types aren't strictly neccessary. They could be implemented with an integer type with invariants.

share|improve this answer
add comment

It's strange that this feature hasn't been added to languages.

Special features for range-limited types are not needed in C++ and other languages with powerful type systems.

In C++, your goals can be met relatively simply with user-defined types. And in applications where range-limited types are desirable, they are hardly sufficient. For example, one would also want the compiler to verify that physical unit computations were written correctly, so that velocity / time produces an acceleration, and taking the square root of acceleration / time produces a velocity. Doing this conveniently requires the ability to define a system of types, without explicitly naming every type that could ever appear in a formula. This can be done in C++.

share|improve this answer
add comment

Some restricted form of your intention is to my knowledge possible in Java and C# through a combination of Annotations and Dynamic Proxy Pattern (there exist built-in implementations for dynamic proxies in Java and C#).

Java version

The annotation:

@Target(ElementType.PARAMETER)
@Inherited
@Retention(RetentionPolicy.RUNTIME)
public @interface IntRange {
     int min ();
     int max ();
}

The Wrapper class creating the Proxy instance:

public class Wrapper {
    public static Object wrap(Object obj) {
        return Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getInterfaces(), new MyInvocationHandler(obj));
    }
}

The InvocationHandler serving as bypass at every method call:

public class MyInvocationHandler implements InvocationHandler {
    private Object impl;

    public MyInvocationHandler(Object obj) {
        this.impl = obj;
    }

@Override
public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable {
    Annotation[][] parAnnotations = method.getParameterAnnotations();
    Annotation[] par = null;
    for (int i = 0; i<parAnnotations.length; i++) {
        par = parAnnotations[i];
        if (par.length > 0) {
            for (Annotation anno : par) {
                if (anno.annotationType() == IntRange.class) {
                    IntRange range = ((IntRange) anno);
                    if ((int)args[i] < range.min() || (int)args[i] > range.max()) {
                        throw new Throwable("int-Parameter "+(i+1)+" in method \""+method.getName()+"\" must be in Range ("+range.min()+","+range.max()+")"); 
                    }
                }
            }
        }
    }
    return method.invoke(impl, args);
}
}

The Example-Interface for Usage:

public interface Example {
    public void print(@IntRange(min=0,max=100) int num);
}

Main-Method:

Example e = new Example() {
    @Override
    public void print(int num) {
        System.out.println(num);
    }
};
e = (Example)Wrapper.wrap(e);
e.print(-1);
e.print(10);

Output:

Exception in thread "main" java.lang.reflect.UndeclaredThrowableException
at com.sun.proxy.$Proxy0.print(Unknown Source)
at application.Main.main(Main.java:13)
Caused by: java.lang.Throwable: int-Parameter 1 in method "print" must be in Range (0,100)
at application.MyInvocationHandler.invoke(MyInvocationHandler.java:27)
... 2 more

C#-Version

The annotation (in C# called attribute):

[AttributeUsage(AttributeTargets.Parameter)]
public class IntRange : Attribute
{
    public IntRange(int min, int max)
    {
        Min = min;
        Max = max;
    }

    public virtual int Min { get; private set; }

    public virtual int Max { get; private set; }
}

The DynamicObject Sub-Class:

public class DynamicProxy : DynamicObject
{
    readonly object _target;

    public DynamicProxy(object target)
    {
        _target = target;
    }

    public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
    {
        TypeInfo clazz = (TypeInfo) _target.GetType();
        MethodInfo method = clazz.GetDeclaredMethod(binder.Name);
        ParameterInfo[] paramInfo = method.GetParameters();
        for (int i = 0; i < paramInfo.Count(); i++)
        {
            IEnumerable<Attribute> attributes = paramInfo[i].GetCustomAttributes();
            foreach (Attribute attr in attributes)
            {
                if (attr is IntRange)
                {
                    IntRange range = attr as IntRange;
                    if ((int) args[i] < range.Min || (int) args[i] > range.Max)
                        throw new AccessViolationException("int-Parameter " + (i+1) + " in method \"" + method.Name + "\" must be in Range (" + range.Min + "," + range.Max + ")");
                }
            }
        }

        result = _target.GetType().InvokeMember(binder.Name, BindingFlags.InvokeMethod, null, _target, args);

        return true;
    }
}

The ExampleClass:

public class ExampleClass
{
    public void PrintNum([IntRange(0,100)] int num)
    {
        Console.WriteLine(num.ToString());
    }
}

Usage:

    static void Main(string[] args)
    {
        dynamic myObj = new DynamicProxy(new ExampleClass());
        myObj.PrintNum(99);
        myObj.PrintNum(-5);
    }

In conclusion, you see that you can get something like that to work in Java, but it's not entirely convenient, because

  • Proxy class can just be instantiated for interfaces, i.e. your class has to implement an interface
  • Allowed Range can only be declared on interface level
  • Later usage comes just with extra effort in the beginning (MyInvocationHandler, wrapping at every instantiation) which also slightly reduces understandability

The capabilities of DynamicObject class in C# remove the interface restriction, as you see in the C# implementation. Unfortunately, this dynamic behavior removes static type safety in in this case, so runtime checks are necessary to determine if a method call on the dynamic proxy is allowed.

If those restrictions are acceptable for you, then this can serve as a basis for further digging!

share|improve this answer
1  
thanks, this is an awesome answer. Is something like this possible in C#? –  Mathew Foscarini May 21 '13 at 20:16
1  
Just added a sample C# implementation! –  McMannus May 21 '13 at 22:23
    
Just FYI: public virtual int Min { get; private set; } is a nice trick that would shorten your code significantly –  BlueRaja - Danny Pflughoeft May 21 '13 at 23:13
2  
This is totally different from what the Q is about, the reason being what you're doing is basically dynamics; which is the antithesis of typing where this question is asking for a type, the difference being when the range is on a type, it's enforced at compile time not run-time. Nobody asked about how to validate ranges at run time, he wanted it validated by the type system which is checked at compile time. –  Jimmy Hoffa May 22 '13 at 14:28
1  
@JimmyHoffa ah that makes sense. Good point :) –  Mathew Foscarini May 22 '13 at 16:02
show 4 more comments

Ada also is a language that allows limits for simple types, in fact in Ada it's good practice to define your own types for your program to guarantee correctness.

type MyType1   is range    1 .. 100;
type MyType2   is range    5 .. 15;

myVar1 : MyType1;

It was used for a long time by the DoD, maybe still is but I've lost track of it's current use.

share|improve this answer
2  
Ada is still widely used in safety critical systems. There is a recent update to the language making the language one of the best available today for writing reliable and maintainable software. Unfortunately the tool support (compilers, IDEs test frameworks etc) is expensive and lagging behind making it hard and unproductive to work with. –  mattnz May 22 '13 at 0:06
    
Shame, I remember using it for the first time and was amazed at how clear and bug free it made code. Glad to hear it's still actively updated, still a great language. –  greedybuddha May 22 '13 at 0:08
    
@mattnz: GNAT is part of the gcc suite, and exists in both free and for-pay versions. –  Keith Thompson May 22 '13 at 23:52
    
@keith: GNAT Compiler is free. IDEs and frameworks are still expensive and lack functionality. –  mattnz May 23 '13 at 1:21
add comment

See Limiting range of value types in C++ for examples of how to create a range-checked value type in C++.

Executive summary: Use a template to create a value type that has built-in minimum and maximum values, which you can use like this:

// create a float named 'percent' that's limited to the range 0..100
RangeCheckedValue<float, 0, 100> percent(50.0);

You don't really even need a template here; you could use a class to similar effect. Using a template lets you specify the underlying type. Also, it's important to note that the type of percent above won't be a float, but rather an instance of the template. This may not satisfy the 'simple types' aspect of your question.

It's strange that this feature hasn't been added to languages.

Simple types are just that -- simple. They're often best used as the building blocks for creating the tools you need instead of being used directly.

share|improve this answer
2  
@JimmyHoffa While I suppose there are some cases where a compiler can detect out of range conditions, range checking mostly needs to happen at run time. The compiler can't possibly know if the value that you download from a web server will be in range, or if the user will add one too many records to a list, or whatever. –  Caleb May 22 '13 at 15:34
add comment

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.