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?
14
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/ ).
5
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.
4
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.
1
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!
9
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.
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++.