Un langage de programmation qui vous permet de définir de nouvelles limites pour les types simples

19

De nombreux langages comme C++, C#et Javavous permettent de créer des objets qui représentent des types simples comme integerou float. À l'aide d'une interface de classe, vous pouvez remplacer les opérateurs et effectuer une logique telle que la vérification si une valeur dépasse une règle métier de 100.

Je me demande s'il est possible dans certains langages de définir ces règles comme des annotations ou des attributs d'une variable / propriété.

Par exemple, dans C#vous pourriez écrire:

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

Ou peut-être que C++vous pourriez écrire:

int(0,100) x = 0;

Je n'ai jamais rien vu de tel, mais étant donné à quel point nous sommes devenus dépendants de la validation des données avant le stockage. Il est étrange que cette fonctionnalité n'ait pas été ajoutée aux langues.

Pouvez-vous donner un exemple de langues où cela est possible?

Reactgular
la source
14
N'est-ce pas Ada quelque chose comme ça?
zxcdw
2
@zxcdw: Oui, Ada était la première langue (comme je le sais) qui a intégré la prise en charge de ces "types". Types de données contraints nommés.
m0nhawk
4
Toutes les langues typées de manière dépendante auraient cette capacité. Il est intrinsèque au système de type en.wikipedia.org/wiki/Dependent_type de manière réaliste, bien que vous puissiez également créer un type personnalisé de cette nature dans n'importe quel ML, dans ces langues, un type est défini comme data Bool = True | Falseet pour ce que vous voulez, vous pourriez dire data Cents = 0 | 1 | 2 | ...avoir un regardez les "Types de données algébriques" (qui devraient être plus correctement nommés types hindley-milner mais les gens confondent cela avec l'inférence de type de manière ennuyeuse) en.wikipedia.org/wiki/Algebraic_data_type
Jimmy Hoffa
2
Compte tenu de la façon dont les langues que vous nommez traitent les débordements et les débordements d'entiers, une telle restriction de plage à elle seule ne vaudrait pas grand-chose si vous gardez le débordement / débordement silencieux.
9
@StevenBurnap: Les types ne nécessitent pas OO. Il y a un typemot - clé dans Pascal après tout. L'orientation des objets est plus un modèle de conception qu'une propriété "atomar" des langages de programmation.
wirrbel

Réponses:

26

Pascal avait des types de sous-gamme, c'est-à-dire une diminution du nombre de nombres qui entrent dans une variable.

  TYPE name = val_min .. val_max;

Ada a également une notion de plages: 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); 

peut aussi faire

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

Et c'est là que ça devient cool

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

C n'a pas de type de sous-gamme strict, mais il existe des moyens d'en imiter un (au moins limité) en utilisant des champs de bits pour minimiser le nombre de bits utilisés. struct {int a : 10;} my_subrange_var;}. Cela peut fonctionner comme une limite supérieure pour le contenu variable (en général, je dirais: n'utilisez pas de champs de bits pour cela , c'est juste pour prouver un point).

De nombreuses solutions pour les types entiers de longueur arbitraire dans d'autres langages se produisent plutôt au niveau de la bibliothèque, c'est-à-dire que C ++ permet des solutions basées sur des modèles.

Il existe des langages qui permettent de surveiller les états variables et d'y associer des assertions. Par exemple dans Clojurescript

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

(def A (atom 0 :validator mytest))

La fonction mytestest appelée quand aa changé (via reset!ou swap!) vérifie si les conditions sont remplies. Cela pourrait être un exemple pour implémenter un comportement de sous-gamme dans des langages à liaison tardive (voir http://blog.fogus.me/2011/09/23/clojurescript-watchers-and-validators/ ).

wirrbel
la source
2
Si vous ajoutiez un détail sur les types dépendants, ce serait bien, ce problème est le but et la raison de la frappe dépendante, semble-t-il au moins être mentionné (même s'il est ésotérique)
Jimmy Hoffa
Même si j'ai une certaine compréhension des types dépendants et du raisonnement inductif / inférence de type milner. J'ai peu de pratique avec ça. Si vous souhaitez ajouter des informations à ma réponse, n'hésitez pas à les modifier. J'allais ajouter quelque chose sur les axiomes de Peano et les types de nombres en mathématiques par définition inductive, mais un bel exemple de données ML pourrait peut-être être plus utile.
wirrbel
vous pouvez claquer un type de plage en C en utilisant enum
John Cartwright
1
enum est afaik de type int ou unsigned int (je pense qu'il est spécifique au compilateur) et n'est pas lié.
wirrbel
Cela devient plus cool que cela: les types à distance peuvent être utilisés dans les déclarations de tableaux et pour les boucles for y in Year_Type loop ... éliminant les problèmes tels que les dépassements de tampon.
Brian Drummond
8

Ada est également un langage qui permet des limites pour les types simples, en fait, dans Ada, il est de bonne pratique de définir vos propres types pour votre programme afin de garantir l'exactitude.

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

myVar1 : MyType1;

Il a été utilisé pendant longtemps par le DoD, peut-être l'est toujours mais j'ai perdu la trace de son utilisation actuelle.

greedybuddha
la source
2
Ada est encore largement utilisé dans les systèmes critiques pour la sécurité. Il y a une mise à jour récente de la langue qui fait de la langue l'une des meilleures disponibles aujourd'hui pour écrire des logiciels fiables et maintenables. Malheureusement, la prise en charge des outils (compilateurs, cadres de test IDE, etc.) est coûteuse et en retard, ce qui rend son travail difficile et improductif.
mattnz
Dommage, je me souviens de l'avoir utilisé pour la première fois et j'ai été étonné de voir à quel point le code était clair et sans bogue. Heureux d'apprendre qu'il est toujours activement mis à jour, toujours un excellent langage.
greedybuddha
@mattnz: GNAT fait partie de la suite gcc et existe en versions gratuite et payante.
Keith Thompson
@keith: GNAT Compiler est gratuit. Les IDE et les frameworks sont toujours chers et manquent de fonctionnalités.
mattnz
7

Voir Limitation de la plage de types de valeurs en C ++ pour des exemples de création d'un type de valeur avec plage vérifiée en C ++.

Résumé: utilisez un modèle pour créer un type de valeur qui a des valeurs minimale et maximale intégrées, que vous pouvez utiliser comme ceci:

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

Vous n'avez même pas vraiment besoin d'un modèle ici; vous pouvez utiliser une classe à effet similaire. L'utilisation d'un modèle vous permet de spécifier le type sous-jacent. En outre, il est important de noter que le type percentci-dessus ne sera pas un float, mais plutôt une instance du modèle. Cela peut ne pas satisfaire l'aspect «types simples» de votre question.

Il est étrange que cette fonctionnalité n'ait pas été ajoutée aux langues.

Les types simples ne sont que cela - simples. Il est souvent préférable de les utiliser comme blocs de construction pour créer les outils dont vous avez besoin au lieu d'être utilisés directement.

Caleb
la source
2
@JimmyHoffa Bien que je suppose qu'il existe certains cas où un compilateur peut détecter des conditions hors plage, la vérification de la plage doit principalement se produire au moment de l'exécution. Le compilateur ne peut pas savoir si la valeur que vous téléchargez à partir d'un serveur Web sera dans la plage, ou si l'utilisateur ajoutera un trop d'enregistrements à une liste, ou autre chose.
Caleb
7

À ma connaissance, une forme restreinte de votre intention est possible en Java et C # grâce à une combinaison d'annotations et de modèle de proxy dynamique (il existe des implémentations intégrées pour les proxys dynamiques en Java et C #).

Version Java

L'annotation:

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

La classe Wrapper créant l'instance de proxy:

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

Le InvocationHandler servant de contournement à chaque appel de méthode:

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

L'exemple d'interface d'utilisation:

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

Méthode principale:

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

Production:

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

Version C #

L'annotation (en C # appelé attribut):

[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 sous-classe 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 classe d'exemple:

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

En conclusion, vous voyez que vous pouvez faire fonctionner quelque chose comme ça en Java , mais ce n'est pas tout à fait pratique, car

  • La classe proxy peut simplement être instanciée pour les interfaces, c'est-à-dire que votre classe doit implémenter une interface
  • La plage autorisée ne peut être déclarée qu'au niveau de l'interface
  • Une utilisation ultérieure vient juste avec un effort supplémentaire au début (MyInvocationHandler, encapsulant à chaque instanciation), ce qui réduit également légèrement la compréhension

Les capacités de la classe DynamicObject en C # suppriment la restriction d'interface, comme vous le voyez dans l'implémentation C #. Malheureusement, ce comportement dynamique supprime la sécurité de type statique dans ce cas, des vérifications d'exécution sont donc nécessaires pour déterminer si un appel de méthode sur le proxy dynamique est autorisé.

Si ces restrictions sont acceptables pour vous, cela peut servir de base à des recherches supplémentaires!

McMannus
la source
1
merci, c'est une réponse géniale. Est-ce que quelque chose comme ça est possible en C #?
Reactgular
1
Je viens d'ajouter un exemple d'implémentation C #!
McMannus
Just FYI: public virtual int Min { get; private set; }est une bonne astuce qui raccourcirait considérablement votre code
BlueRaja - Danny Pflughoeft
2
C'est totalement différent de ce que le Q traite, la raison étant que ce que vous faites est fondamentalement de la dynamique; qui est l'antithèse de la frappe où cette question demande un type , la différence étant que lorsque la plage est sur un type, elle est appliquée au moment de la compilation et non à l'exécution. Personne n'a demandé comment valider les plages au moment de l'exécution, il voulait qu'il soit validé par le système de type qui est vérifié au moment de la compilation.
Jimmy Hoffa
1
@JimmyHoffa ah ça a du sens. Bon point :)
Reactgular
2

Les plages sont un cas particulier des invariants. De Wikipédia:

Un invariant est une condition qui peut être invoquée pour être vraie lors de l'exécution d'un programme.

Une plage [a, b]peut être déclarée comme une variable x de type Integeravec les invariants x> = a et x <= b .

Par conséquent, les types de sous-plages Ada ou Pascal ne sont pas strictement nécessaires. Ils pourraient être implémentés avec un type entier avec des invariants.

nalply
la source
0

Il est étrange que cette fonctionnalité n'ait pas été ajoutée aux langues.

Les fonctionnalités spéciales pour les types à plage limitée ne sont pas nécessaires en C ++ et dans d'autres langages dotés de systèmes de types puissants.

En C ++, vos objectifs peuvent être atteints relativement simplement avec des types définis par l' utilisateur . Et dans les applications où des types à plage limitée sont souhaitables, ils ne sont guère suffisants . Par exemple, on voudrait également que le compilateur vérifie que les calculs d'unité physique ont été écrits correctement, de sorte que la vitesse / temps produit une accélération et que la racine carrée de l'accélération / temps produise une vitesse. Pour ce faire, il est nécessaire de pouvoir définir un système de types sans nommer explicitement tous les types pouvant apparaître dans une formule. Cela peut être fait en C ++ .

Kevin Cline
la source