Uma linguagem de programação que permite definir novos limites para tipos simples

19

Muitas línguas gosto C++, C#e Javapermitem que você crie objetos que representam tipos simples, como integerou float. Usando uma interface de classe, você pode substituir os operadores e executar lógica como verificar se um valor excede uma regra de negócios de 100.

Gostaria de saber se em alguns idiomas é possível definir essas regras como anotações ou atributos de uma variável / propriedade.

Por exemplo, C#você pode escrever:

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

Ou talvez C++você possa escrever:

int(0,100) x = 0;

Eu nunca vi algo assim ser feito, mas, considerando o quão dependentes nos tornamos da validação de dados antes do armazenamento. É estranho que esse recurso não tenha sido adicionado aos idiomas.

Você pode dar exemplos de idiomas onde isso é possível?

Reactgular
fonte
14
Ada não é algo assim?
Zxcdw 21/05
2
@zxcdw: Sim, Ada foi o primeiro idioma (como eu sei) que criou suporte para esses "tipos". Tipos de dados restritos nomeados.
M0nhawk
4
Todos os idiomas digitados com dependência teriam essa capacidade. É intrínseco ao sistema de tipo en.wikipedia.org/wiki/Dependent_type realista que você poderia criar um tipo personalizado desta natureza em qualquer ML, bem como, em línguas um tipo é definido como data Bool = True | Falsee para o que você deseja que você poderia dizer data Cents = 0 | 1 | 2 | ...ter um olhada em "tipos algébrica de dados" (que deve ser mais propriamente chamados tipos Hindley-Milner, mas as pessoas confundem isso com a inferência de tipos irritantemente) en.wikipedia.org/wiki/Algebraic_data_type
Jimmy Hoffa
2
Dado como os idiomas que você nomeia lidam com overflow e underflow inteiro, essa restrição de intervalo por si só não valeria muito se você mantivesse o silencioso over / underflow.
9
@StevenBurnap: Os tipos não requerem OO. typeAfinal, existe uma palavra - chave em Pascal. A orientação a objetos é mais um padrão de design do que uma propriedade "atomar" das linguagens de programação.
Wirrbel

Respostas:

26

Pascal tinha tipos de subintervalos, ou seja, diminuindo o número de números que se encaixam em uma variável.

  TYPE name = val_min .. val_max;

Ada também tem uma noção de intervalos: http://en.wikibooks.org/wiki/Ada_Programming/Types/range

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

também pode fazer

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

E aqui é onde fica legal

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

C não possui um tipo estrito de subintervalo, mas existem maneiras de imitar um (pelo menos limitado) usando campos de bits para minimizar o número de bits usados. struct {int a : 10;} my_subrange_var;}. Isso pode funcionar como um limite superior para o conteúdo variável (em geral, eu diria: não use campos de bits para isso , isso é apenas para provar um ponto).

Muitas soluções para tipos inteiros de comprimento arbitrário em outras linguagens acontecem no nível da biblioteca, ou seja, o C ++ permite soluções baseadas em modelo.

Existem idiomas que permitem monitorar estados variáveis ​​e conectar asserções a ele. Por exemplo, em Clojurescript

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

(def A (atom 0 :validator mytest))

A função mytesté chamada quando amudou (via reset!ou swap!) verifica se as condições são atendidas. Este poderia ser um exemplo para implementar o comportamento de subintervalos em idiomas de ligação tardia (consulte http://blog.fogus.me/2011/09/23/clojurescript-watchers-and-validators/ ).

wirrbel
fonte
2
Se você gostaria de acrescentar um detalhe sobre os tipos de dependentes assim seria bom, este problema é todo o propósito e razão para a digitação dependente, parece que deve pelo menos ser mencionado (mesmo que seja esotérico)
Jimmy Hoffa
Embora eu tenha alguma compreensão dos tipos dependentes e do raciocínio indutivo / inferência do tipo milner. Eu tenho pouca prática com isso. Se você quiser adicionar informações à minha resposta, sinta-se à vontade para editá-la. Eu adicionaria algo sobre os axiomas de Peano e os tipos de números em matemática por definição indutiva, mas um bom exemplo de dados de ML talvez valesse mais a pena.
Wirrbel #
você poderia kludge um tipo de intervalo em C usando enum
John Cartwright
1
enum é um tipo de tipo int ou unsigned int (acho que é específico do compilador) e não é verificado.
Wirrbel
Fica mais legal que isso: os tipos variados podem ser usados ​​em declarações de array e para loops, for y in Year_Type loop ... eliminando problemas como estouros de buffer.
Brian Drummond
8

O Ada também é uma linguagem que permite limites para tipos simples; de fato, no Ada, é uma boa prática definir seus próprios tipos para o seu programa para garantir a correção.

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

myVar1 : MyType1;

Foi usado por um longo tempo pelo Departamento de Defesa, talvez ainda seja, mas eu perdi a noção do seu uso atual.

greedybuddha
fonte
2
O Ada ainda é amplamente utilizado em sistemas críticos de segurança. Há uma atualização recente do idioma, tornando o idioma um dos melhores disponíveis atualmente para a criação de software confiável e de manutenção. Infelizmente, o suporte da ferramenta (compiladores, estruturas de teste de IDEs etc.) é caro e está atrasado, tornando difícil e improdutivo trabalhar com ele.
mattnz
Que pena, eu lembro de usá-lo pela primeira vez e fiquei surpreso com o quão claro e livre de erros tornava o código. Fico feliz em saber que ele ainda é atualizado ativamente, ainda é um ótimo idioma.
greedybuddha
@mattnz: O GNAT faz parte do pacote gcc e existe nas versões gratuita e paga.
Keith Thompson
@keith: O GNAT Compiler é gratuito. IDEs e estruturas ainda são caras e carecem de funcionalidade.
mattnz
7

Consulte Limitando o intervalo de tipos de valor em C ++ para obter exemplos de como criar um tipo de valor verificado no intervalo em C ++.

Resumo executivo: Use um modelo para criar um tipo de valor com valores mínimo e máximo integrados, que você pode usar assim:

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

Você nem precisa de um modelo aqui; você pode usar uma classe para um efeito semelhante. O uso de um modelo permite especificar o tipo subjacente. Além disso, é importante observar que o tipo depercent acima não será um float, mas uma instância do modelo. Isso pode não satisfazer o aspecto "tipos simples" da sua pergunta.

É estranho que esse recurso não tenha sido adicionado aos idiomas.

Tipos simples são exatamente isso - simples. Eles geralmente são mais usados ​​como os blocos de construção para criar as ferramentas necessárias em vez de serem usadas diretamente.

Caleb
fonte
2
@JimmyHoffa Embora suponha que haja alguns casos em que um compilador possa detectar condições fora do intervalo, a verificação do intervalo geralmente precisa ocorrer em tempo de execução. O compilador não pode saber se o valor que você baixa de um servidor da Web estará dentro do alcance ou se o usuário adicionará muitos registros a uma lista ou o que for.
Caleb
7

De acordo com o meu conhecimento, alguma forma restrita de sua intenção é possível em Java e C # através de uma combinação de anotações e padrão de proxy dinâmico (existem implementações internas para proxies dinâmicos em Java e C #).

Versão Java

A anotação:

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

A classe Wrapper que cria a instância do Proxy:

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

O InvocationHandler servindo como desvio a cada chamada de 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);
}
}

A interface de exemplo para 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);

Resultado:

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

Versão C #

A anotação (em C # chamada 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; }
}

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

A ExampleClass:

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

Em conclusão, você vê que pode conseguir que algo assim funcione em Java , mas não é totalmente conveniente, porque

  • A classe de proxy pode ser instanciada apenas para interfaces, ou seja, sua classe precisa implementar uma interface
  • O intervalo permitido só pode ser declarado no nível da interface
  • O uso posterior vem com um esforço extra no início (MyInvocationHandler, encapsulando a cada instanciação), o que também reduz um pouco a capacidade de compreensão

Os recursos da classe DynamicObject em C # removem a restrição de interface, como você vê na implementação de C #. Infelizmente, esse comportamento dinâmico remove a segurança do tipo estático nesse caso, portanto, são necessárias verificações de tempo de execução para determinar se uma chamada de método no proxy dinâmico é permitida.

Se essas restrições são aceitáveis ​​para você, isso pode servir de base para novas escavações!

McMannus
fonte
1
obrigado, esta é uma resposta incrível. É possível algo assim em C #?
Reactgular
1
Acabei de adicionar uma amostra de implementação em C #!
McMannus
Apenas FYI: public virtual int Min { get; private set; }é um truque agradável que iria encurtar o código significativamente
BlueRaja - Danny Pflughoeft
2
Isso é totalmente diferente do que é o Q, a razão pela qual você está fazendo é basicamente dinâmica; que é a antítese da digitação, onde esta pergunta está solicitando um tipo , a diferença é que quando o intervalo está em um tipo, ela é aplicada no tempo de compilação e não no tempo de execução. Ninguém perguntou sobre como validar intervalos em tempo de execução, ele queria que fosse validado pelo sistema de tipos que é verificado em tempo de compilação.
Jimmy Hoffa
1
@JimmyHoffa ah isso faz sentido. Bom ponto :)
Reactgular
2

As faixas são um caso especial de invariantes. Da Wikipedia:

Uma invariante é uma condição na qual se pode confiar durante a execução de um programa.

Um intervalo [a, b]pode ser declarado como uma variável x do tipo Integercom os invariantes x> = a e x <= b .

Portanto, os tipos de subintervalo Ada ou Pascal não são estritamente necessários. Eles podem ser implementados com um tipo inteiro com invariantes.

nalply
fonte
0

É estranho que esse recurso não tenha sido adicionado aos idiomas.

Recursos especiais para tipos de alcance limitado não são necessários no C ++ e em outros idiomas com sistemas de tipos avançados.

No C ++, seus objetivos podem ser alcançados de maneira relativamente simples com tipos definidos pelo usuário . E em aplicações em que tipos de alcance limitado são desejáveis, dificilmente são suficientes . Por exemplo, também se deseja que o compilador verifique se os cálculos das unidades físicas foram escritos corretamente, para que a velocidade / tempo produza uma aceleração e a raiz quadrada da aceleração / tempo produza uma velocidade. Fazer isso convenientemente requer a capacidade de definir um sistema de tipos, sem nomear explicitamente todos os tipos que possam aparecer em uma fórmula. Isso pode ser feito em C ++ .

Kevin Cline
fonte