Un lenguaje de programación que le permite definir nuevos límites para tipos simples

19

Muchos idiomas C++, como , C#y le Javapermiten crear objetos que representan tipos simples como integero float. Usando una interfaz de clase, puede anular operadores y realizar lógica como verificar si un valor excede una regla comercial de 100.

Me pregunto si es posible en algunos idiomas definir estas reglas como anotaciones o atributos de una variable / propiedad.

Por ejemplo, en C#usted podría escribir:

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

O tal vez en C++ti podrías escribir:

int(0,100) x = 0;

Nunca he visto algo así, pero dado lo dependientes que nos hemos vuelto de la validación de datos antes del almacenamiento. Es extraño que esta característica no se haya agregado a los idiomas.

¿Puedes dar ejemplos de idiomas donde esto sea posible?

Reactgular
fuente
14
¿No es Ada algo como esto?
zxcdw
2
@zxcdw: Sí, Ada fue el primer idioma (como sé) que ha incorporado soporte para tales "tipos". Tipos de datos restringidos con nombre.
m0nhawk
44
Todos los idiomas de tipo dependiente tendrían esta capacidad. Es intrínseco al sistema de tipos en.wikipedia.org/wiki/Dependent_type de manera realista, aunque también podría crear un tipo personalizado de esta naturaleza en cualquier ML, en estos idiomas un tipo se define como data Bool = True | Falsey por lo que quiere, podría decir que data Cents = 0 | 1 | 2 | ...tiene un mire "Tipos de datos algebraicos" (que deberían denominarse más correctamente tipos hindley-milner, pero la gente lo confunde con la inferencia de tipos de manera molesta) es.wikipedia.org/wiki/Algebraic_data_type
Jimmy Hoffa
2
Dada la forma en que los idiomas que nombra manejan el desbordamiento y el desbordamiento de enteros, tal restricción de rango por sí sola no valdría mucho si mantiene el desbordamiento / desbordamiento silencioso.
99
@StevenBurnap: los tipos no requieren OO. Hay una typepalabra clave en Pascal después de todo. La orientación a objetos es más un patrón de diseño que una propiedad "atomar" de los lenguajes de programación.
wirrbel

Respuestas:

26

Pascal tenía tipos de subrango, es decir, disminuía el número de números que encajaban en una variable.

  TYPE name = val_min .. val_max;

Ada también tiene una noción de rangos: http://en.wikibooks.org/wiki/Ada_Programming/Types/range

De 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); 

también puede hacer

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

Y aquí es donde se pone genial

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

C no tiene un tipo de subrango estricto, pero hay formas de imitar uno (al menos limitado) mediante el uso de campos de bits para minimizar el número de bits utilizados. struct {int a : 10;} my_subrange_var;}. Esto puede funcionar como un límite superior para contenido variable (en general, diría: no use bitfields para esto , esto es solo para probar un punto).

Muchas soluciones para tipos enteros de longitud arbitraria en otros lenguajes suceden a nivel de biblioteca, es decir, C ++ permite soluciones basadas en plantillas.

Hay lenguajes que permiten monitorear estados variables y conectar aserciones. Por ejemplo en Clojurescript

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

(def A (atom 0 :validator mytest))

La función mytestse llama cuando aha cambiado (vía reset!o swap!) verifica si se cumplen las condiciones. Este podría ser un ejemplo para implementar el comportamiento de subrango en lenguajes de enlace tardío (consulte http://blog.fogus.me/2011/09/23/clojurescript-watchers-and-validators/ ).

wirrbel
fuente
2
Si agregara un detalle sobre los tipos dependientes también sería bueno, este problema es el único propósito y la razón para la tipificación dependiente, parece que al menos debería mencionarse (incluso si es esotérico)
Jimmy Hoffa
Si bien tengo cierta comprensión de los tipos dependientes y el razonamiento inductivo / inferencia tipo Milner. Tengo poca práctica con esos. Si desea agregar información a mi respuesta, no dude en editarla. Iba a agregar algo sobre los Axiomas de Peano y los tipos de números en matemáticas por definición inductiva, pero un buen ejemplo de datos de ML podría valer más la pena.
wirrbel
podrías evitar un tipo de rango en C usando la enumeración
John Cartwright
1
enum es un afaik de tipo int o unsigned int (creo que es específico del compilador) y no está acotado.
wirrbel
Se vuelve más genial que eso: los tipos a distancia se pueden usar en declaraciones de matriz y para bucles que for y in Year_Type loop ... eliminan problemas como desbordamientos de búfer.
Brian Drummond
8

Ada también es un lenguaje que permite límites para tipos simples, de hecho, en Ada es una buena práctica definir sus propios tipos para su programa para garantizar la corrección.

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

myVar1 : MyType1;

El Departamento de Defensa lo usó durante mucho tiempo, tal vez todavía lo sea, pero he perdido la noción de su uso actual.

greedybuddha
fuente
2
Ada todavía se usa ampliamente en sistemas críticos de seguridad. Hay una actualización reciente del idioma que lo convierte en uno de los mejores disponibles hoy en día para escribir software confiable y fácil de mantener. Desafortunadamente, el soporte de herramientas (compiladores, marcos de prueba de IDE, etc.) es costoso y va a la zaga, por lo que es difícil y poco productivo trabajar con él.
mattnz
Es una pena, recuerdo haberlo usado por primera vez y me sorprendió lo claro y libre de errores que hizo el código. Me alegra saber que todavía se actualiza activamente, sigue siendo un gran idioma.
greedybuddha
@mattnz: GNAT es parte de la suite gcc y existe en versiones gratuitas y de pago.
Keith Thompson
@keith: GNAT Compiler es gratis. Los IDE y los marcos siguen siendo caros y carecen de funcionalidad.
mattnz
7

Ver Limitar el rango de tipos de valor en C ++ para ver ejemplos de cómo crear un tipo de valor de rango verificado en C ++.

Resumen Ejecutivo: use una plantilla para crear un tipo de valor que tenga valores mínimos y máximos integrados, que puede usar de esta manera:

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

Realmente ni siquiera necesitas una plantilla aquí; podrías usar una clase con un efecto similar. El uso de una plantilla le permite especificar el tipo subyacente. Además, es importante tener en cuenta que el tipo de percentarriba no será unfloat , sino una instancia de la plantilla. Es posible que esto no satisfaga el aspecto de "tipos simples" de su pregunta.

Es extraño que esta característica no se haya agregado a los idiomas.

Los tipos simples son solo eso, simples. A menudo se usan mejor como bloques de construcción para crear las herramientas que necesita en lugar de usarse directamente.

Caleb
fuente
2
@JimmyHoffa Aunque supongo que hay algunos casos en los que un compilador puede detectar condiciones fuera de rango, la verificación de rango debe realizarse principalmente en tiempo de ejecución. El compilador no puede saber si el valor que descarga de un servidor web estará dentro del rango, o si el usuario agregará demasiados registros a una lista, o lo que sea.
Caleb
7

Según mi conocimiento, alguna forma restringida de su intención es posible en Java y C # a través de una combinación de Anotaciones y Patrón de proxy dinámico (existen implementaciones incorporadas para proxys dinámicos en Java y C #).

Versión Java

La anotación:

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

La clase Wrapper que crea la instancia de Proxy:

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

El InvocationHandler sirve como bypass en cada llamada a método:

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);
}
}

La interfaz de ejemplo para el uso:

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

Método principal:

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);

Salida:

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

Versión C #

La anotación (en C # llamado atributo):

[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; }
}

La subclase DynamicObject:

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;
    }
}

La clase de ejemplo:

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

Uso:

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

En conclusión, ve que puede hacer que algo así funcione en Java , pero no es del todo conveniente, porque

  • La clase de proxy solo puede ser instanciada para interfaces, es decir, su clase tiene que implementar una interfaz
  • El rango permitido solo se puede declarar en el nivel de interfaz
  • El uso posterior viene solo con un esfuerzo adicional al principio (MyInvocationHandler, envolviendo en cada instanciación) que también reduce ligeramente la comprensibilidad

Las capacidades de la clase DynamicObject en C # eliminan la restricción de la interfaz, como puede ver en la implementación de C #. Desafortunadamente, este comportamiento dinámico elimina la seguridad de tipo estático en este caso, por lo que las verificaciones de tiempo de ejecución son necesarias para determinar si se permite una llamada al método en el proxy dinámico.

Si esas restricciones son aceptables para usted, ¡esto puede servir como base para una mayor excavación!

McMannus
fuente
1
Gracias, esta es una respuesta increíble. ¿Es posible algo así en C #?
Reactgular
1
¡Acabo de agregar una implementación de muestra de C #!
McMannus
Solo para tu información: public virtual int Min { get; private set; }es un buen truco que acortaría significativamente tu código
BlueRaja - Danny Pflughoeft
2
Esto es totalmente diferente de lo que trata la Q, la razón es que lo que estás haciendo es básicamente dinámica; que es la antítesis del tipeo cuando esta pregunta solicita un tipo , la diferencia es que cuando el rango está en un tipo, se aplica en tiempo de compilación, no en tiempo de ejecución. Nadie preguntó sobre cómo validar los rangos en tiempo de ejecución, quería que el sistema de tipos lo validara en tiempo de compilación.
Jimmy Hoffa
1
@JimmyHoffa ah, eso tiene sentido. Buen punto :)
Reactgular
2

Los rangos son un caso especial de invariantes. De Wikipedia:

Una invariante es una condición en la que se puede confiar que sea verdadera durante la ejecución de un programa.

Un rango [a, b]puede declararse como una variable x de tipo Integercon las invariantes x> = a y x <= b .

Por lo tanto, los tipos de subrango Ada o Pascal no son estrictamente necesarios. Podrían implementarse con un tipo entero con invariantes.

finalmente
fuente
0

Es extraño que esta característica no se haya agregado a los idiomas.

No se necesitan características especiales para tipos de rango limitado en C ++ y otros lenguajes con sistemas de tipos potentes.

En C ++, sus objetivos se pueden cumplir de manera relativamente simple con tipos definidos por el usuario . Y en aplicaciones donde los tipos de rango limitado son deseables, apenas son suficientes . Por ejemplo, uno también querría que el compilador verifique que los cálculos de la unidad física se escribieron correctamente, de modo que la velocidad / tiempo produzca una aceleración, y tomar la raíz cuadrada de aceleración / tiempo produzca una velocidad. Hacer esto convenientemente requiere la capacidad de definir un sistema de tipos, sin nombrar explícitamente cada tipo que pueda aparecer en una fórmula. Esto se puede hacer en C ++ .

Kevin Cline
fuente