ReasonML: Tipos variantes polimórficos

Compatibilidade de Tipos, Restrições e Unificações

Esse artigo faz parte da série “O que é ReasonML?”.

Nesse artigo, iremos analisar as variantes polimórficas, que são uma versão mais flexível das variantes normais. Mas essa flexibilidade também a torna mais complicada.

[Nota: Esse artigo foi um desafio para escrever e é com base em vários recursos na web. Correções e dicas são bem-vindo!]

1. O que são variantes polimórficas?

Variantes polimórficas são semelhantes às variantes normais. A maior diferença é que os construtores não estão ligados aos tipos. Eles existem de forma independente. Isso deixa as variantes polimórficas mais versáteis e leva a consequências interessantes em nosso sistema de tipos. Algumas dessas consequências são negativas — é o preço que se paga a versatilidade.

Vamos começar com a seguinte variante normal e em seguida, transformá-la em uma variante polimórfica.

type rgb = Red | Green | Blue;

A versão polimórfica do rgb é definida assim:

type rgb = [`Red | `Green | `Blue];

Os construtores de uma variante polimórfica devem ser colocados entre colchetes e seus nomes devem começar com backticks seguido por letras minúsculas ou maiusculas:

# `red;
- : [> `red ] = `red
# `Red;
- : [> `Red ] = `Red

Como de costume, tipos como rgb devem começar com letras minúsculas.

1.1 Construtores polimórficos existem na própria

Você só pode usar um construtor não polimórfico como parte de uma variante não polimórfica:

# Int(123);
Error: Unbound constructor Int
# type data = Int(int) | Str(string);
type data = Int(int) | Str(string);
# Int(123);
- : data = Int(123)

Em contraste, você pode usar construtores polimórficos. Não há nenhuma necessidade de defini-los de antemão:

# `Int(123);
- : [> `Int(int) ] = `Int(123)

Observe que `Int(123) tem um tipo interessante: [> `Int(int) ]. Vamos examinar o que isso significa mais tarde.

Construtores polimórficos são independentes, permitindo que o mesmo Construtor seja usado mais de uma vez:

type color = [`Red | `Orange | `Yellow | `Green | `Blue | `Purple];
type rgb = [`Red | `Green | `Blue];

Em contraste, com variantes normais, você deve evitar múltiplas variantes dentro do mesmo escopo com o mesmo construtor.

Você ainda pode usar o construtor polimórfico mesmo com diferentes quantidades e/ou parâmetros de tipo:

# `Int("abc", true);
- : [> `Int((string, bool)) ] = `Int(("abc", true))
# `Int(1.0, 2.0, 3.0);
- : [> `Int((float, float, float)) ] = `Int((1., 2., 3.))

1.2 Estendendo as variantes polimórficas

Ao definir uma variante polimórfica, você pode estender uma variante existente. No exemplo a seguir, a color estende rgb:

type rgb = [`Red | `Green | `Blue];
type color = [rgb | `Orange | `Yellow | `Purple];

Como você pode ver, o caso importa aqui: rgb sendo minúscula indica que seus construtores são inseridos.

Também é possível estender múltiplas variantes:

type red = [ `Red ];
type green = [ `Green ];
type blue = [ `Blue ];
type rgb = [ red | green | blue ];

1.3 O namespace de construtores polimórficos é global

Os nomes dos construtores não polimórficos são de parte de seus escopos (por exemplo, seus módulos), Porém, o namespace de construtores polimórficos é global. Você pode ver isso no exemplo a seguir:

module M = {
type data = [`Int(int) | `Str(string)];
let stringOfData = (x: data) =>
switch x {
| `Int(i) => string_of_int(i)
| `Str(s) => s
};
};
M.stringOfData(`Int(123));

Na última linha, o parâmetro de M.stringOfData() é criado através de um construtor que não é do escopo do M. Devido o namespace global dos construtores polimórficos, o parâmetro deve ser compatível com o tipo data.

1.4 Compatibilidade de tipo

Como que o ReasonML determina se o tipo de um parâmetro real (usado na chamada da função) é compatível com o tipo do parâmetro formal (definição da função)? Vamos ver alguns exemplos. Vamos começar criando um tipo e uma função cujo único parâmetro é desse tipo.

# type rgb = [`Red | `Green | `Blue];
type rgb = [ `Blue | `Green | `Red ];
# let id = (x: rgb) => x;
let id: (rgb) => rgb = <fun>;

Em seguida, criamos um novo de tipo rgb2 que tem os mesmos construtores polimórficos que rgb e iremos chamar id() com um valor cujo tipo é rgb2:

# type rgb2 = [`Red | `Green | `Blue];
type rgb2 = [ `Blue | `Green | `Red ];
# let arg: rgb2 = `Blue;
let arg: rgb2 = `Blue;
# id(arg);
- : rgb = `Blue

Normalmente, o tipo do parâmetro formal x e o parâmetro real arg tem que ser o mesmo. Mas com variantes polimórficas, se seus tipos têm os mesmos construtores, já é suficiente.

No entanto, se o tipo de um parâmetro real, arg2, tem menos construtores, ReasonML não te deixa fazer a chamada de função:

# type rg = [`Red | `Green];
type rg = [ `Green | `Red ];
# let arg2: rg = `Red;
let arg2: rg = `Red;
# id(arg2);
Error: This expression has type rg but an expression was expected of type rgb
The first variant type does not allow tag(s) `Blue

A razão é que id faz exigências precisas sobre o que precisa: exatamente os construtores `Red, `Green e `Blue. Portanto, o tipo de rg não é suficiente.

De tipos concretos para restrições de tipos

Curiosamente, o seguinte código funciona:

# type rgb = [`Red | `Green | `Blue];
type rgb = [ `Blue | `Green | `Red ];
# let id = (x: rgb) => x;
let id: (rgb) => rgb = <fun>;
# let arg3 = `Red;
let arg3: [> `Red ] = `Red;
# id(arg3);
- : rgb = `Red

Por que? Porque ReasonML não infere um tipo fixo para arg3, é inferido uma restrição de tipo [> `Red ]. Essa restrição é um chamado limite inferior e significa: "todos os tipos que têm que ter pelo menos o construtor `Red". Essa restrição é compatível com rgb, que é o tipo de x.

Restrições de tipo para parâmetros

Podemos também usar restrições de tipos em parâmetros. Por exemplo, a seguinte chamada de id() falha, porque o tipo do parâmetro, rgbw, tem muitos construtores:

# type rgbw = [`Red | `Green | `Blue | `White ];
type rgbw = [ `Blue | `Green | `Red | `White ];
# id(`Red: rgbw);
Error: This expression has type rgbw but an expression was expected of type rgb
The second variant type does not allow tag(s) `White

Dessa vez, chamamos id() e especificamos o tipo do parâmetro rgbw diretamente (sem a ligação intermediária let ). Nós podemos fazer a função functionar sem alterar a chamada de função, alterando apenas a id():

# let id = (x: [> `Red | `Green | `Blue]) => x;
let id: (([> `Blue | `Green | `Red ] as 'a)) => 'a = <fun>;
# id(`Red: rgbw); /* same as before */
- : rgbw = `Red

Agora x tem um limite inferior e aceita todos os tipos que têm pelo menos três construtores determinados.

Você também pode definir restrições, referindo-se às variantes. Por exemplo, a seguinte definição de id() é basicamente equivalente ao anterior.

let id = (x: [> rgb]) => x;

Vamos dar uma olhada melhor em restrições de tipo mais tarde.

2. Escrevendo código extensível com variantes polimórficas

Uma vantagem chave de variantes polimórficas é que o código se torna mais extensível.

2.1 O tipo e o código que queremos estender

Como exemplo, vamos ver as seguintes definições de tipo de formas. Eles são versões polimórficas do exemplo do artigo anterior.

type point = [ `Point(float, float) ];
type shape = [
| `Rectangle(point, point)
| `Circle(point, float)
];

Olhando essas definições de tipo, podemos escrever uma função que calcula a área de uma forma:

let pi = 4.0 *. atan(1.0);
let computeArea = (s: shape) =>
switch s {
| `Rectangle(`Point(x1, y1), `Point(x2, y2)) =>
let width = abs_float(x2 -. x1);
let height = abs_float(y2 -. y1);
width *. height;
| `Circle(_, radius) => pi *. (radius ** 2.0)
};

2.2 Estendendo shape: uma primeira tentativa para falhar

Digamos que queremos estender shape com uma forma a mais – triângulos. Como faríamos isso? Podemos simplesmente definir um novo de tipo shapePlus que reutiliza os construtores polimórficos existentes, Rectangle e Circlee adicionar o construtor Triangle:

type shapePlus = [
| `Rectangle(point, point)
| `Circle(point, float)
| `Triangle(point, point, point)
];

Agora também temos de estender computeArea(). A função a seguir é a nossa primeira tentativa de escrever essa extensão:

let shoelaceFormula = (`Point(x1, y1), `Point(x2, y2), `Point(x3, y3)) =>
0.5 *. abs_float(x1*.y2 -. x3*.y2 +. x3*.y1 -. x1*.y3 +. x2*.y3 -. x2*.y1);
let computeAreaPlus = (sp: shapePlus) =>
switch sp {
| `Triangle(p1, p2, p3) => shoelaceFormula(p1, p2, p3)
| `Rectangle(_, _) => computeArea(sp) /* A */
| `Circle(_, _) => computeArea(sp) /* B */
};

Infelizmente, esse código não funciona: nas linhas A e B, sp é do tipo shapePlus e não é compatível com o tipo shape do parâmetro de computeArea. Nós temos a seguinte mensagem de erro:

Error: This expression has type shapePlus
but an expression was expected of type shape
The second variant type does not allow tag(s) `Triangle

2.3 Arrumando computeAreaPlus através de uma cláusula "as"

Felizmente, nós podemos corrigir o problema usando a cláusula as nos dois últimos casos:

let computeAreaPlus = (sp: shapePlus) =>
switch sp {
| `Triangle(p1, p2, p3) => shoelaceFormula(p1, p2, p3)
| `Rectangle(_, _) as r => computeArea(r)
| `Circle(_, _) as c => computeArea(c)
};

Como as nos ajuda? Com variantes polimórficas, ela escolhe o tipo mais geral possível. Isto é:

  • r tem o tipo [> `Rectangle(point, point)]
  • c tem o tipo [> `Circle(point, float)]

Esses dois tipos são compatíveis com shape, o tipo de parâmetro de computeArea.

2.4 A solução final

Há uma melhoria que nós podemos fazer. Dada a seguinte variante:

type myvariant = [`C1(t1) | `C2(t2)];

Os seguintes dois padrões são equivalentes:

#myvariant
(`C1(_: t1) | `C2(_: t2))

Se usarmos o hash para o tipo de shape, nós temos:

let computeAreaPlus = (sp: shapePlus) =>
switch sp {
| `Triangle(p1, p2, p3) => shoelaceFormula(p1, p2, p3)
| #shape as s => computeArea(s)
};

Vamos usar o computeAreaPlus() com duas formas:

let top = `Point(3.0, 5.0);
let left = `Point(0.0, 0.0);
let right = `Point(3.0, 0.0);
let circ = `Circle(top, 3.0);
let tri = `Triangle(top, left, right);
computeAreaPlus(circ); /* 28.274333882308138 */
computeAreaPlus(tri); /* 7.5 */

Portanto, com sucesso, ampliamos tanto o tipo de shape e a função computeArea().

3. Melhores práticas: variantes normais vs variantes polimórficas

Em geral, você deve preferir variantes normais sobre variantes polimórficas, porque elas são ligeiramente mais eficientes e ativam a verificação de tipo mais rigorosa. Citando o manual OCaml:

[…] variantes polimórficas, sendo tipadas, resultam em uma disciplina de tipo mais fraca. Ou seja, as variantes normais fazem realmente muito mais do que garantir a segurança de tipos, elas também verificam que você use somente construtores declarados, que todos os construtores presentes em uma estrutura de dados são compatíveis, e elas impõe restrições de digitação de seus parâmetros.

No entanto, variantes polimórficas tem alguns pontos fortes bem claros. Se qualquer um desses pontos é importante para uma determinada situação, você deve usar variantes polimórficas:

  • Reutilização: Um Construtor (possivelmente junto com um código para processá-lo) é útil para mais de uma variante. Construtores para cores caem nesta categoria, por exemplo.
  • Dissociação: Um construtor é usado em vários locais, mas você não quer que esses lugares dependam de um único módulo, onde o construtor é definido. Em vez disso, você pode simplesmente usar o construtor sem defini-lo.
  • Extensibilidade: Você espera uma variante ser estendida mais tarde. Semelhante a como shapePlus era uma extensão de shape, no início desse artigo.
  • Concisão: Devido ao namespace global dos construtores polimórficos, você pode usá-los sem qualificá-los ou abrir seus módulos (veja próxima subseção).
  • Usar construtores sem definição anterior: você pode usar construtores polimórficos sem defini-los de antemão através de variantes. Às vezes é conveniente para tipos que são usados somente em locais simples.

Felizmente, caso haja a necessidade, é relativamente fácil alterar uma variante normal para uma variante polimórfica.

3.1 Concisão: variantes normais vs variantes polimórficas

Vamos comparar a concisão das variantes normais e variantes polimórficas.

Para a variante normal bwNormal, você precisa qualificar Black na linha A (ou abrir seu módulo):

module MyModule = {
type bwNormal = Black | White;
let getNameNormal(bw: bwNormal) =
switch bw {
| Black => "Black"
| White => "White"
};
};
print_string(MyModule.getNameNormal(MyModule.Black)); /* A */
/* "Black" */

Para a variante polimórfica bwPoly, não há necessidade de qualificação em `Black na linha A:

module MyModule = {
type bwPoly = [ `Black | `White ];
let getNamePoly(bw: bwPoly) =
switch bw {
| `Black => "Black"
| `White => "White"
};
};
print_string(MyModule.getNamePoly(`Black)); /* A */
/* "Black" */

A necessidade de qualificar a variante neste exemplo não surtiu muito efeito, porém, é importante saber, caso você use os construtores muitas vezes.

3.2 Evitando erros de digitação com variantes polimórficas

Uma questão com variantes polimórficas é que você recebe menos avisos sobre erros de digitação, porque você pode usar construtores sem defini-los. Por exemplo, no código a seguir, `Green é digitado como `Gren na linha A:

type rgb = [`Red | `Green | `Blue];
let f = (x) =>
switch x {
| `Red => "Red"
| `Gren => "Green" /* A */
| `Blue => "Blue"
};
/* let f: ([< `Blue | `Gren | `Red ]) => string = <fun>; */

Você receberá um alerta se você adicionar uma anotação de tipo para o parâmetro x:

let f = (x: rgb) =>
switch x {
| `Red => "Red"
| `Gren => "Green"
| `Blue => "Blue"
};
/*
Error: This pattern matches values of type [? `Gren ]
but a pattern was expected which matches values of type rgb
The second variant type does not allow tag(s) `Gren
*/

Se você retornar valores variantes polimórficos de uma função, você também pode especificar o tipo de retorno desta função. Mas isso também adiciona peso ao seu código, então certifique-se de que você irá realmente se beneficiar com isso. Se você tipar todos os parâmetros, muitos, se não a maioria dos problemas, devem ser detectados.

4. Tópicos avançados

Todas as seções a seguir cobrem tópicos avançados.

5. Restrições de tipo para variáveis com tipo

Antes de prosseguir, vamos falar mais sobre restrições para variantes polimórficas, mas antes, precisamos entender as restrições gerais para variáveis com tipo (dos quais o primeiro, é um caso especial).

No final de uma definição de tipo, pode haver uma ou mais restrições de tipo. Estes têm a seguinte sintaxe:

constraint «typeexpr» = «typeexpr»

Essas restrições são utilizadas para refinar as variáveis com tipo, na definição de tipo. Este é um exemplo simples:

type t('a) = 'a constraint 'a=int;

Como o ReasonML lida com restrições? Antes de falarmos sobre isso, vamos primeiro entender unificação e como ela baseia-se no pattern matching.

pattern matching vai em uma única direção: um termo sem variáveis é usado para preencher as variáveis em um outro termo. No exemplo a seguir, `Int(123) é o termo sem variáveis e `Int(x) é o termo com variáveis:

switch(`Int(123)) {
| `Int(x) => print_int(x)
}

Unificação é o pattern matching que funciona nos dois sentidos: podemos ter variáveis e variáveis em ambos os lados serão preenchidas. Como exemplo, considere:

# type t('a, 'b) = ('a, 'b) constraint ('a, int) = (bool, 'b);
type t('a, 'b) = ('a, 'b) constraint 'a = bool constraint 'b = int;

ReasonML simplifica tanto quanto possível: a restrição complexa original, com variáveis em ambos os lados do sinal de igualdade, foi convertida em duas restrições simples com variáveis somente do lado esquerdo.

Este é um exemplo onde as coisas podem ser simplificadas, tanto que nenhuma restrição é necessária:

# type t('a, 'b) = 'c constraint 'c = ('a, 'b);
type t('a, 'b) = ('a, 'b);

6. Restrições de tipo para variantes polimórficas

As restrições de tipo que vimos no início deste artigo são na verdade apenas as restrições de tipo específicas para variantes polimórficas.

Por exemplo, as duas expressões seguintes são equivalentes:

let x: [> `Red ] = `Red;
let x: [> `Red] as 'a = `Red;

Por outro lado, as expressões de dois tipo a seguir também são equivalentes (mas você não pode usar constraint com as ligações e as definições de parâmetro criadas por let ):

[> `Red] as 'a
'a constraint 'a = [> `Red ]

Ou seja, com todas as restrições de variantes polimórficas que usamos até agora, sempre havia uma variável com tipo implícita (ou oculta). Podemos ver que, se tentarmos usar tal restrição para definir um tipo t:

# type t = [> `Red ];
Error: A type variable is unbound in this type declaration.
In type [> `Red ] as 'a the variable 'a is unbound

Podemos resolver isto da seguinte maneira. Observe o tipo final calculado por ReasonML.

# type t('a) = [> `Red] as 'a;
type t('a) = 'a constraint 'a = [> `Red ];

6.1 Limites superiores e inferiores para variantes polimórficas

Para o restante deste artigo, nos referimos às restrições de tipo de variantes polimórficas como simplesmente tipo de limitações ou restrições.

Restrições de tipo consistem em um ou ambos dos seguintes procedimentos:

  • Um limite inferior: indica quais elementos um tipo devem conter, no mínimo. Por exemplo: [> `Red | `Green] aceita todos os tipos que incluem os construtores `Red e `Green. Em outras palavras: como um conjunto, a restrição aceita todos os tipos que são superconjuntos do mesmo.
  • Um limite superior: indica quais elementos um tipo deve conter, no máximo. Por exemplo: [< `Red | `Green] aceita os seguintes tipos: [`Red | `Green], [`Red], [`Green].

Você pode usar restrições de tipo para:

  • Definições de tipo
  • Ligações let
  • Definições de parâmetro

Para os dois últimos, você deve usar a forma abreviada (sem constraint).

7. Como as restrições de tipos encontram um valor?

7.1 Limites inferiores

Vamos examinar como o limite inferior [> `Red | `Green] funciona, iremos usá-lo como o tipo de um parâmetro de função x:

let lower = (x: [> `Red | `Green]) => true;

Valores do tipo [`Red | `Green | `Blue] e [`Red | `Green] são aceitos:

# lower(`Red: [`Red | `Green | `Blue]);
- : bool = true
# lower(`Red: [`Red | `Green]);
- : bool = true

No entanto, os valores do tipo [`Red] não são aceitos, porque esse tipo não contém ambos os construtores da restrição.

# lower(`Red: [`Red]);
Error: This expression has type [ `Red ]
but an expression was expected of type [> `Green | `Red ]
The first variant type does not allow tag(s) `Green

7.2 Limites superiores

As seguintes experiências tem como o limite superior [< `Red | `Green]:

# let upper = (x: [< `Red | `Green]) => true;
let upper: ([< `Green | `Red ]) => bool = <fun>;
# upper(`Red: [`Red | `Green]); /* OK */
- : bool = true
# upper(`Red: [`Red]); /* OK */
- : bool = true
# upper(`Red: [`Red | `Green | `Blue]);
Error: This expression has type [ `Blue | `Green | `Red ]
but an expression was expected of type [< `Green | `Red ]
The second variant type does not allow tag(s) `Blue

8. Restrições de tipo inferido

Se você usar construtores polimórficas, ReasonML infere restrições de tipo para você. Vejamos alguns exemplos.

8.1 Limites inferiores

# `Int(3);
- : [> `Int(int) ] = `Int(3)

O valor `Int(3) tem o tipo inferido [> `Int(int) ] que é compatível com todos os tipos que têm pelo menos o construtor `Int(int).

Com uma tupla, você tem duas restrições de tipo inferido separado:

# (`Red, `Green);
- : ([> `Red ], [> `Green ]) = (`Red, `Green)

Por outro lado, os elementos de uma lista, devem ser todos do mesmo tipo, sendo assim, ReasonML consegue mesclar os dois tipos inferidos de restrições:

# [`Red, `Green];
- : list([> `Green | `Red ]) = [`Red, `Green]

Nessa lista, o tipo esperado é uma lista com elementos cujo tipo inclui pelo menos os construtores `Red e `Green.

Se você tentar usar o mesmo construtor com parâmetros de tipos diferentes, você obterá um erro, porque o ReasonML não consegue mesclar os dois tipos inferidos:

# [`Int(3), `Int("abc")];
Error: This expression has type string but
an expression was expected of type int

8.2 Limites superiores

Até agora, vimos apenas limites inferiores sendo inferidos. No exemplo a seguir, ReasonML infere um limite superior para o parâmetro x:

let f = (x) =>
switch x {
| `Red => 1
| `Green => 2
};
/* let f: ([< `Green | `Red ]) => int = <fun>; */

Devido a expressão switch , f pode lidar com no máximo dois construtores `Red e `Green.

O tipo inferido torna-se mais complexo se f retornar seu parâmetro:

let f = (x) =>
switch x {
| `Red => x
| `Green => x
};
/* let f: (([< `Green | `Red ] as 'a)) => 'a = <fun>; */

O parâmetro de tipo 'a é usado para expressar que o tipo do parâmetro e o tipo de resultado são os mesmos.

As coisas mudam se usarmos a cláusula as, porque ela irá dissasociar o tipo de entrada do tipo de saída:

let f = (x) =>
switch x {
| `Red as r => r
| `Green as g => g
};
/* let f: ([< `Green | `Red ]) => [> `Green | `Red ] = <fun>; */

8.3 Os limites do sistema de tipos do ReasonML

Algumas coisas vão além das capacidades do sistema de tipos do ReasonML:

let f = (x) =>
switch x {
| `Red => x
| `Green => `Blue
};
/*
Error: This expression has type [> `Blue ]
but an expression was expected of type [< `Green | `Red ]
The second variant type does not allow tag(s) `Blue
*/

O tipo de retorno de f é: "o tipo de x ou o tipo de [> `Blue]". No entanto, não há nenhuma maneira de expressá-lo através de uma restrição.

8.4 Restrições mais complexas

Restrições de tipo podem se tornar bastante complexas. Vamos começar com duas funções simples:

let even1 = (x) =>
switch x {
| `Data(n) => (n mod 2) == 0
};
/* let even1: ([< `Data(int) ]) => bool = <fun>; */
let even2 = (x) =>
switch x {
| `Data(s) => (String.length(s) mod 2) == 0
};
/* let even2: ([< `Data(string) ]) => bool = <fun>; */

Ambos os tipos inferidos incluem limites superiores, causados pelas declarações de switch .

Vamos usar a mesma variável x como um parâmetro para ambas as funções:

let even = (x) => even1(x) && even2(x);
/* let even: ([< `Data(string & int) ]) => bool = <fun>; */

Para tipar x, ReasonML mescla dois tipos a seguir:

[< `Data(int) ]
[< `Data(string) ]

O resultado contém o seguinte construtor.

`Data(string & int)

O que significa “um construtor `Data" cujo parâmetro de tipo tem dois tipo string e tipo int. Esse construtor não existe, o que significa que você não pode chamar even(), porque não há nenhum valor que é compatível com o tipo do seu parâmetro.

9. Inferência de tipo usa Unificação (que é bi-direcional)

Quando ReasonML calcula tipos através de inferência e determina que dois tipos t1, t2 devem ser iguais (por exemplo, o tipo de um parâmetro real) e o tipo de um parâmetro formal, ele usa a unificação para resolver a equação t1 = t2.

Vou demonstrar isso através de várias funções. Cada uma dessas funções retorna seu único parâmetro. O tipo tp especifica o parâmetro, que é diferente do tipo tr, que especifica o resultado. ReasonML vai tentar unificar tp e tr, nos permitindo observar que a Unificação é bi-direcional.

9.1 Duas restrições

Se tanto o tipo de parâmetro e o tipo de resultado são as restrições, o ReasonML tenta mesclar as restrições.

# let f = (x: [>`Red]): [< `Red | `Green] => x;
let f: (([< `Green | `Red > `Red ] as 'a)) => 'a = <fun>;
# let f = (x: [>`Red]): [> `Red | `Green] => x;let f: (([> `Green | `Red ] as 'a)) => 'a = <fun>;

O tipo unificado permanece polimórfico — contém a variável do tipo 'a. A variável de tipo é usada para expressar: “qualquer que seja o tipo de x, o resultado tem o mesmo tipo”.

9.2 Um tipo monomórficas, uma restrição

Se um dos dois tipos é monomórficas (não tem nenhuma variável de tipo) e o outro uma restrição, em seguida, a restrição é usada apenas para verificar o tipo monomórfico. Devido a unificação, ambos os tipos acabam sendo monomórficos.

# let f = (x: [>`Red]): [`Red | `Green] => x;
let f: ([ `Green | `Red ]) => [ `Green | `Red ] = <fun>;
# let f = (x: [`Red]): [< `Red | `Green] => x;
let f: ([ `Red ]) => [ `Red ] = <fun>;

9.3 Dois tipos de monomórficos

Se ambos os tipos são monomórficos, então eles devem ter os mesmos construtores.

# let f = (x: [`Red]): [`Red | `Green] => x;
Error: This expression has type [ `Red ]
but an expression was expected of type [ `Green | `Red ]
The first variant type does not allow tag(s) `Green
# let f = (x: [`Red | `Green]): [`Red | `Green] => x;
let f: ([ `Green | `Red ]) => [ `Green | `Red ] = <fun>;

10. Tipos Monomórficos vs Restrições (constraint)

Uma demonstração da diferença entre o tipo monomórfico e restrição. Considere as duas funções a seguintes:

type rgb = [`Red | `Green | `Blue];let id1 = (x: rgb) => x;
/* let id1: (rgb) => rgb = <fun>; */
let id2 = (x:[>rgb]) => x;
/* let id2: (([> rgb ] as 'a)) => 'a = <fun>; */

Vamos comparar as duas funções:

  • id1() tem um parâmetro cujo tipo rgb é monomórfico (não tem nenhuma variável de tipo).
  • id2() tem um parâmetro cujo tipo [>rgb] é uma restrição. A definição em si não parece polimórfica, mas a assinatura computada é - há uma variável de tipo, 'a.

Vamos ver o que acontece se nós chamarmos essas funções com argumentos cujo tipo é uma nova variante polimórfica que tem os mesmos construtores como rgb:

type c3 = [ `Blue | `Green | `Red ];

Com id1(), o tipo de x, portanto, o resultado, é fixo. Ou seja, ele permanece rgb:

# id1(`Red: c3);
- : rgb = `Red

Só fomos capazes de chamar id1(), pois rgb e c3 são de mesmo tipo.

Em contraste, com id2(), o tipo de x é polimórfico e mais flexível. Durante a unificação, 'a está vinculado a c3. O resultado da chamada de função tem tipo c3 (o valor de 'a).

# id2(`Red: c3);
- : c3 = `Red

11. Material para referência

Variantes polimórficas e como usá-los:

Restrições de variável de tipo:

A semântica das variantes polimórficas:

Créditos

Written by

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store