Język programowania, który pozwala zdefiniować nowe ograniczenia dla prostych typów

19

Wiele języków podoba C++, C#i Javapozwalają na tworzenie obiektów, które reprezentują typów prostych, jak integeri float. Za pomocą interfejsu klasy można przesłonić operatorów i wykonać logikę, np. Sprawdzanie, czy wartość przekracza regułę biznesową równą 100.

Zastanawiam się, czy w niektórych językach można zdefiniować te reguły jako adnotacje lub atrybuty zmiennej / właściwości.

Na przykład C#możesz napisać:

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

A może w C++tobie możesz napisać:

int(0,100) x = 0;

Nigdy nie widziałem czegoś takiego zrobionego, ale biorąc pod uwagę, jak bardzo uzależniliśmy się od sprawdzania poprawności danych przed przechowywaniem. Dziwne, że ta funkcja nie została dodana do języków.

Czy możesz podać przykłady języków, w których jest to możliwe?

Reactgular
źródło
14
Czy Ada nie jest taka?
zxcdw
2
@zxcdw: Tak, Ada była pierwszym językiem (jak wiem), który ma wbudowaną obsługę takich „typów”. Nazwane ograniczone typy danych.
m0nhawk
4
Wszystkie zależne od siebie języki miałyby taką możliwość. Jest to nieodłącznie związane z systemem typów en.wikipedia.org/wiki/Dependent_type realistycznie, chociaż można również utworzyć niestandardowy typ tego rodzaju w dowolnym ML, w tych językach typ jest zdefiniowany jako data Bool = True | Falsei dla tego, co chcesz powiedzieć, data Cents = 0 | 1 | 2 | ...mieć spójrz na „Algebraiczne typy danych” (które powinny być bardziej właściwie nazywane typami Hindley-Milner, ale ludzie mylą to z wnioskowaniem typu irytująco) en.wikipedia.org/wiki/Algebraic_data_type
Jimmy Hoffa
2
Biorąc pod uwagę, w jaki sposób języki, które wymieniasz, obsługują przepełnienie i niedopełnienie liczb całkowitych, takie ograniczenie zakresu samo w sobie nie byłoby warte wiele, gdybyś utrzymywał cichy przepełnienie / niedopełnienie.
9
@StevenBurnap: typy nie wymagają OO. W typekońcu w Pascalu jest słowo kluczowe. Orientacja obiektowa jest bardziej wzorcem projektowym niż „atomową” właściwością języków programowania.
wirrbel

Odpowiedzi:

26

Pascal miał typy podzakresów, tj. Zmniejszając liczbę liczb pasujących do zmiennej.

  TYPE name = val_min .. val_max;

Ada ma również pojęcie zakresów: http://en.wikibooks.org/wiki/Ada_Programming/Types/range

Z Wikipedii ....

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

może również zrobić

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

I tutaj robi się fajnie

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

C nie ma ścisłego typu podzakresu, ale istnieją sposoby naśladowania jednego (przynajmniej ograniczone) za pomocą pól bitowych w celu zminimalizowania liczby używanych bitów. struct {int a : 10;} my_subrange_var;}. Może to działać jako górna granica dla zmiennej treści (ogólnie powiedziałbym: nie używaj do tego pól bitowych , to tylko dowód na to, że punkt).

Wiele rozwiązań dla liczb całkowitych o dowolnej długości w innych językach raczej dzieje się na poziomie biblioteki, tj. C ++ pozwala na rozwiązania oparte na szablonach.

Istnieją języki, które pozwalają monitorować stany zmiennych i łączyć z nimi twierdzenia. Na przykład w Clojurescript

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

(def A (atom 0 :validator mytest))

Funkcja mytestjest wywoływana, gdy azmieniła się (przez reset!lub swap!) sprawdza, czy warunki są spełnione. Może to być przykład implementacji zachowania podzakresów w późno wiążących językach (patrz http://blog.fogus.me/2011/09/23/clojurescript-watchers-and-validators/ ).

wirrbel
źródło
2
Przydałby się również szczegół na temat typów zależnych. Byłby to cały cel i powód pisania zależnego, wydaje się, że należy go przynajmniej wspomnieć (nawet jeśli jest ezoteryczny)
Jimmy Hoffa
Chociaż rozumiem typy zależne i wnioskowanie indukcyjne / wnioskowanie typu milnera. Mam z nimi małą praktykę. Jeśli chcesz dodać informacje do mojej odpowiedzi, możesz je edytować. Chciałem dodać coś o aksjomatach Peano i typach liczbowych w matematyce według definicji indukcyjnej, ale może być bardziej przydatny przykład danych ML.
wirrbel
możesz zaklinować typ zakresu w C za pomocą wyliczenia
John Cartwright
1
enum jest afaik typu int lub unsigned int (myślę, że jest specyficzny dla kompilatora) i nie jest sprawdzane pod kątem ograniczeń.
wirrbel
Robi się chłodniej: typy dystansowe mogą być używane w deklaracjach tablicowych i pętlach, for y in Year_Type loop ... eliminując problemy takie jak przepełnienie bufora.
Brian Drummond
8

Ada jest także językiem, który dopuszcza ograniczenia dla prostych typów, w rzeczywistości w Adzie dobrą praktyką jest definiowanie własnych typów dla swojego programu, aby zagwarantować poprawność.

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

myVar1 : MyType1;

Był używany przez długi czas przez DoD, może nadal jest, ale straciłem świadomość jego obecnego użycia.

greedybuddha
źródło
2
Ada jest nadal szeroko stosowana w systemach krytycznych dla bezpieczeństwa. Niedawna aktualizacja języka czyni go jednym z najlepszych dostępnych obecnie do pisania niezawodnego i łatwego w utrzymaniu oprogramowania. Niestety obsługa narzędzi (kompilatory, frameworki testowe IDE itp.) Jest droga i pozostaje w tyle, co utrudnia i nieproduktywność w pracy.
mattnz
Szkoda, pamiętam, że używałem go po raz pierwszy i byłem zaskoczony, jak czysty i wolny od błędów kod tworzył. Cieszę się, że wciąż jest aktywnie aktualizowany, wciąż świetny język.
greedybuddha
@mattnz: GNAT jest częścią pakietu gcc i istnieje zarówno w wersji darmowej, jak i płatnej.
Keith Thompson
@keith: Kompilator GNAT jest bezpłatny. IDE i frameworki są wciąż drogie i brakuje im funkcjonalności.
mattnz
7

Zobacz Ograniczanie zakresu typów wartości w C ++, aby dowiedzieć się, jak utworzyć typ wartości z kontrolą zakresu w C ++.

Streszczenie: Użyj szablonu, aby utworzyć typ wartości, który ma wbudowane wartości minimalne i maksymalne, których możesz użyć w następujący sposób:

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

Tak naprawdę nie potrzebujesz tutaj szablonu; możesz użyć klasy z podobnym skutkiem. Korzystanie z szablonu pozwala określić typ bazowy. Ponadto należy zauważyć, że typpercent powyższym nie będzie float, ale raczej instancja szablonu. To może nie spełniać aspektu „prostych typów” pytania.

Dziwne, że ta funkcja nie została dodana do języków.

Proste typy są po prostu takie - proste. Często są najlepiej stosowane jako elementy składowe do tworzenia potrzebnych narzędzi zamiast bezpośredniego użycia.

Caleb
źródło
2
@JimmyHoffa Chociaż przypuszczam, że są pewne przypadki, w których kompilator może wykryć warunki poza zakresem, sprawdzanie zasięgu najczęściej musi odbywać się w czasie wykonywania. Kompilator nie może wiedzieć, czy wartość, którą pobierasz z serwera WWW, będzie w zakresie, czy użytkownik doda jeden lub wiele rekordów do listy, czy cokolwiek innego.
Caleb
7

Pewna ograniczona forma twojego zamiaru jest według mojej wiedzy możliwa w Javie i C # poprzez kombinację Adnotacji i Dynamicznego Wzorca Proxy (istnieją wbudowane implementacje dla dynamicznych proxy w Javie i C #).

Wersja Java

Adnotacja:

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

Klasa Wrapper tworząca instancję proxy:

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

InvocationHandler służący jako obejście przy każdym wywołaniu metody:

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

Przykładowy interfejs użytkowania:

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

Główna metoda:

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

Wynik:

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

Wersja C #

Adnotacja (w języku C # o nazwie atrybut):

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

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

The ExampleClass:

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

Stosowanie:

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

Podsumowując, widzisz, że możesz dostać coś takiego do pracy w Javie , ale nie jest to do końca wygodne, ponieważ

  • Można utworzyć instancję klasy proxy dla interfejsów, tzn. Klasa musi implementować interfejs
  • Dozwolony zakres można zadeklarować tylko na poziomie interfejsu
  • Późniejsze użycie jest na początku z dodatkowym wysiłkiem (MyInvocationHandler, owijanie przy każdej instancji), co również nieznacznie zmniejsza zrozumiałość

Możliwości klasy DynamicObject w C # usuwają ograniczenia interfejsu, jak widać w implementacji C #. Niestety, takie zachowanie dynamiczne usuwa w tym przypadku bezpieczeństwo typu statycznego, dlatego konieczne jest sprawdzenie środowiska wykonawczego w celu ustalenia, czy dozwolone jest wywołanie metody na dynamicznym serwerze proxy.

Jeśli ograniczenia te są dla Ciebie dopuszczalne, może to stanowić podstawę do dalszego kopania!

McMannus
źródło
1
dzięki, to świetna odpowiedź. Czy coś takiego jest możliwe w C #?
Reactgular
1
Właśnie dodałem przykładową implementację C #!
McMannus,
Po prostu FYI: public virtual int Min { get; private set; }to fajna sztuczka, która znacznie skróciłaby Twój kod
BlueRaja - Danny Pflughoeft
2
Jest to całkowicie odmienne od tego, o co chodzi w Q, ponieważ przyczyną tego, co robisz, jest w zasadzie dynamika; który jest antytezą pisania na klawiaturze, gdy to pytanie wymaga typu , z tą różnicą, że gdy zakres jest na typie, jest wymuszany w czasie kompilacji, a nie w czasie wykonywania. Nikt nie pytał o sposób sprawdzania poprawności zakresów w czasie wykonywania, chciał tylko, aby sprawdził to przez system typów sprawdzany w czasie kompilacji.
Jimmy Hoffa
1
@ JimmyHoffa ah, to ma sens. Dobra uwaga :)
Reactgular
2

Zakresy są szczególnym przypadkiem niezmienników. Z Wikipedii:

Niezmienna jest warunek, że można powołać się być prawdą podczas realizacji programu.

Przedział [a, b]może być uznana jako zmienna x typu Integerz Niezmienniki x> = a i x <= b .

Dlatego typy podzakresów Ada lub Pascal nie są absolutnie konieczne. Można je zaimplementować za pomocą liczb całkowitych z niezmiennikami.

dokładnie
źródło
0

Dziwne, że ta funkcja nie została dodana do języków.

Specjalne funkcje dla typów ograniczonych zasięgiem nie są potrzebne w C ++ i innych językach z potężnymi systemami typów.

W C ++ twoje cele mogą być osiągnięte stosunkowo prosto dzięki typom zdefiniowanym przez użytkownika . A w aplikacjach, w których pożądane są typy o ograniczonym zasięgu, nie są one wystarczające . Na przykład, chcielibyśmy również, aby kompilator sprawdził, czy obliczenia jednostek fizycznych zostały poprawnie zapisane, tak aby prędkość / czas wytwarzała przyspieszenie, a przyjmowanie pierwiastka kwadratowego przyspieszenia / czas dawało prędkość. Wykonanie tego w wygodny sposób wymaga umiejętności zdefiniowania systemu typów, bez wyraźnego nazywania każdego typu, który mógłby kiedykolwiek pojawić się w formule. Można to zrobić w C ++ .

Kevin Cline
źródło