ReasonML: Funções

Terminologias, recursividade e anotações

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

Nesse artigo, iremos explorar como as funções funcionam no ReasonML.

1. Definindo de funções

  • 1.1 Funções como parâmetros de outras funções (high-order functions)
  • 1.2 Blocos como corpos de função
  • 1.3 Parâmetros únicos sem parênteses
  • 1.4 Evitando avisos sobre parâmetros não utilizados

2. Recursividade com let rec

  • 2.1 Definindo funções mutuamente recursivas
  • 2.2 Definindo funções auto-recursivas

3. Terminologia: aridade (arity)

4. Os tipos de funções

  • 4.1 Tipos de função de primeira ordem (first-order function)
  • 4.2 Tipos de função de ordem superior (high-order function)
  • 4.3 Anotações de tipos e Inferências de tipos
  • 4.4 As anotações de tipo podem levar a tipos inferidos mais específicos
  • 4.5 Prática recomendada: anote todos os parâmetros

5. Não há funções sem parâmetros

  • 5.1 Por que não há funções nulas (nullary functions)?

6. Desestruturação de parâmetros da função

7. Parâmetros rotulados

  • 7.1 Compatibilidade de tipos de função com parâmetros rotulados

8. Parâmetros opcionais

  • 8.1 Com parâmetros opcionais, você precisa de pelo menos um parâmetro de posição
  • 8.2 Escreva anotações para parâmetros opcionais
  • 8.3 Valores padrões de parâmetros
  • 8.4 Escreva anotações com valores padrão de parâmetros
  • 8.5 Passando valores option para parâmetros opcionais (avançado)

9. Aplicação parcial

  • 9.1 Por que a aplicação parcial é útil?
  • 9.2 Aplicação parcial e parâmetros rotulados
  • 9.2.1 Aplicação parcial e parâmetros opcionais
  • 9.3 Currying (avançado)

10 .O operador de aplicação reversa (|>)

  • 10.1 Exemplo: encadeando ints e strings
  • 10.2 Exemplo: encadeando Lists
  • 10.3 Alerta: encadeando para funções unárias

11. Dicas para criar assinaturas de função

12. Abreviação de funções unárias com switch

13. Alerta: avançado

14. Operadores

  • 14.1 Regras para operadores
  • 14.2 Operadores de precedência e associatividade
  • 14.3 Quando a associatividade é importante?

15. Funções polimórficas

  • 15.1 Exemplo: id()
  • 15.2 Exemplo: first()
  • 15.3 Exemplo: ListLabels.map()
  • 15.4 Sobrecarga vs. polimorfismo paramétrico

16. ReasonML não suporta funções variadicas

1. Definindo de funções

Uma função anônima (sem nome) é a seguinte:

(x) => x + 1;

Esta função possui um único parâmetro, x e o corpo x + 1.

Você pode atribuir a essa função um nome, vinculando-a a uma variável:

let plus1 = (x) => x + 1;

E assim, podemos chamar plus1:

# plus1(5);
- : int = 6

As funções também podem ser parâmetros de outras funções. Para demonstrar esse recurso, usaremos um curto exemplo com Lists, que serão explicadas em detalhes em um artigo futuro. As Lists são, aproximadamente, listas ligadas isoladamente (linked lists) e similares a arrays imutáveis.

A função List.map (func, list) recebe uma lista e aplica func a cada um de seus elementos e retorna os resultados em uma nova lista. Por exemplo:

# List.map((x) => x + 1, [12, 5, 8, 4]);
- : list(int) = [13, 6, 9, 5]

Funções que têm funções como parâmetros ou seus resultados, são chamadas high-order functions. Funções que não são têm esses atributos, não são chamadas de high-order functions. List.map() é uma high-order functions, já plus1(), não é uma high-order functions.

O corpo de uma função é uma expressão. Dado que, blocos de escopo são expressões, as duas definições a seguir para plus1 são equivalentes.

let plus1 = (x) => x + 1;let plus1 = (x) => {
x + 1
};

Se uma função tiver um único parâmetro e esse parâmetro for definido através de um identificador, você pode omitir os parênteses:

let plus1 = x => x + 1;

Os compiladores do ReasonML e os plugins do editor avisam sobre variáveis não utilizadas. Por exemplo, para a função a seguir, você recebe o aviso “unused variable y”.

let getFirstParameter = (x, y) => x;

Você pode evitar o aviso prefixando os nomes das variáveis não utilizadas com sublinhados:

let getFirstParameter = (x, _y) => x;

Você também pode usar o nome da variável _, várias vezes:

let foo = (_, _) => 123;

2. Recursividade com let rec

Normalmente, você só pode se referir a valores let que já foram definidos. Isso significa que você não pode definir, mutualmente, funções recursivas e auto-recursivas com let.

Examinemos as funções mutuamente recursivas primeiro. As duas funções seguintes, even e odd, são mutuamente recursivas. Você deve usar o operador especial let rec para defini-las:

let rec even = (x) =>
switch x {
| n when n < 0 => even(-x) /* A */
| 0 => true /* B */
| 1 => false /* C */
| n => odd(n - 1)
}
and odd = (x) => even(x - 1);

Observe como and conecta várias entradas de let rec, fazendo com que elas conheçam umas as outras. Não há ponto-e-vírgula antes do and. O ponto-e-vírgula no final indica que let rec está finalizado.

even e odd são definidas da seguinte maneira:

  • Um inteiro x é even se x - 1 for odd
  • Um inteiro x é odd se x - 1 for even

Obviamente, isso não pode continuar para sempre, então precisamos definir casos em even() (linhas B e C). Nós também cuidamos dos números inteiros negativos (linha A).

Vamos usar essas funções:

# even(11);
- : bool = false
# odd(11);
- : bool = true
# even(24);
- : bool = true
# odd(24);
- : bool = false
# even(-1);
- : bool = false
# odd(-1);
- : bool = true

Você também precisa de let rec para funções auto-recursivas, pois quando a chamada recursiva ocorre, a ligação (binding) não existe ainda. Por exemplo:

let rec factorial = (x) =>
if (x <= 0) {
1
} else {
x * factorial(x - 1)
};
factorial(3); /* 6 */
factorial(4); /* 24 */

3. Terminologia: aridade (arity)

A aridade de uma função é o número de seus parâmetros posicionais. Por exemplo, a aridade de factorial() é 1. Os seguintes adjetivos descrevem funções com aridade de 0 a 3:

  • Uma função nullary, contém uma aridade 0
  • Uma função unary, contém uma aridade 1
  • Uma função binary, contém uma aridade 2
  • Uma função ternary, contém uma aridade 3

A partir de 3, nós falamos de função 4-ary, 5-ary, etc. Funções que variam em aridade, são chamadas de funções variadic (variadic functions).

4. Os tipos de funções

Utilizando funções, é a primeira vez que entramos em contato com tipos complexos: tipos criados ao combinar outros tipos. Vamos usar a linha de comando rtop do ReasonML para determinar os tipos de duas funções.

Primeiramente, uma função add():

# let add = (x, y) => x + y;
let add: (int, int) => int = <fun>;

Portanto, o tipo dela é:

(int, int) => int

A seta indica que que add é uma função. Seus parâmetros são dois ints. O resultado é um único int.

A notação (int, int) => int também é chamada de [tipo de] assinatura ([type] signature) de add. Ele descreve os tipos de suas entradas e suas saídas.

Vamos definir uma high-order function chamada callFunc():

# let callFunc = (f) => f(1) + f(2);
let callFunc: ((int) => int) => int = <fun>;

Você pode ver que o parâmetro de callFunc em si, é uma função e tem o tipo (int) => int.

E chamamos calFunc dessa maneiera:

# callFunc(x => x);
- : int = 3
# callFunc(x => 2 * x);
- : int = 6

Em algumas linguagens de programação estáticamente tipadas, você deve fornecer anotações de tipo para todos os parâmetros e o valor de retorno de uma função. Por exemplo:

# let add = (x: int, y: int): int => x + y;
let add: (int, int) => int = <fun>;

As duas primeiras ocorrências de : int são anotações de tipo para os parâmetros x e y. O último : int declara o resultado de add.

No entanto, o ReasonML permite que você omita o tipo de retorno. Em seguida, ele usa inferência de tipo para deduzir o tipo de retorno baseado nos tipos dos parâmetros e do corpo da função:

# let add = (x: int, y: int) => x + y;
let add: (int, int) => int = <fun>;

No entanto, a inferência de tipos em ReasonML é ainda mais sofisticada do que isso. Não só deduz os tipos de cima para baixo (dos parâmetros ao corpo até o resultado). Mas também pode deduzir os tipos de baixo para cima. Por exemplo, ReasonML pode inferir que x e y são ints, porque eles estão utilizando o operador de inteiros +:

# let add = (x, y) => x + y;
let add: (int, int) => int = <fun>;

Em outras palavras: a maioria das anotações de tipo no ReasonML são opcionais.

Mesmo que as anotações de tipo sejam opcionais, ao fornecê-las, muitas vezes, elas aumentam a especificidade dos tipos. Considere, por exemplo, a seguinte função createIntPair:

# let createIntPair = (x, y) => (x, y);
let createIntPair: ('a, 'b) => ('a, 'b) = <fun>;

Todos os tipos que o ReasonML infere são chamados de variáveis de tipo (type variables). Eles começam com apóstrofos e significam “qualquer tipo” (any type). Eles serão explicados com mais detalhes mais tarde.

Se anotarmos os parâmetros, obtemos tipos mais específicos:

# let createIntPair = (x: int, y: int) => (x, y);
let createIntPair: (int, int) => (int, int) = <fun>;

Também obtemos tipos mais específicos se anotarmos apenas o valor de retorno:

# let createIntPair = (x, y): (int, int) => (x, y);
let createIntPair: (int, int) => (int, int) = <fun>;

O estilo de codificação que eu prefiro para funções é anotar todos os parâmetros, mas permitir que o ReasonML infira o tipo de retorno. Além de melhorar a verificação do tipo, as anotações para parâmetros também são boas documentações (que são verificadas automaticamente, mantendo à consistência).

5. Não há funções sem parâmetros

Não há funções nullary no ReasonML, mas você raramente irá notar quando usá-las.

Por exemplo, se você definir uma função sem parâmetros, o ReasonML adiciona um parâmetro para você, cujo tipo é unit:

# let f = () => 123;
let f: (unit) => int = <fun>;

Lembre-se de que o tipo unit, só tem um elemento único (), que significa “sem valor” (é aproximadamente igual ao null em muitos idiomas de estilo C).

Ao chamar funções, omitindo seus parâmetros, é o mesmo que passar () como um único parâmetro:

# f();
- : int = 123
# f(());
- : int = 123

A interação a seguir, é outra demonstração deste fenômeno: se você chamar uma função unária, sem parâmetros, o rtop sublinha () e reclama que essa expressão tem o tipo errado.

Notavelmente, não se queixa de parâmetros faltantes (e também, não os aplica parcialmente — detalhes sobre isso mais tarde).

# let times2 = (x) => x * 2;
let times2: (int) => int = <fun>;
# times2();
Error: This expression has type unit but
an expression was expected of type int

Para resumir, o ReasonML não possui funções nulas, mas esconde esse fato ao definir e ao chamar as funções.

Por que o ReasonML não possui funções nulas? Isso é devido a ReasonML sempre executar uma aplicação parcial (explicada em detalhes mais adiante): se você não fornecer todos os parâmetros de uma função, você obtém uma nova função com os parâmetros restantes para o resultado. Como conseqüência, se você realmente não puder fornecer nenhum parâmetro, então func() seria o mesmo que func. Ou seja, o primeiro exemplo não faria nada.

6. Desestruturação de parâmetros da função

A desestruturação pode ser usada sempre que as variáveis estão vinculadas a valores: como em expressões let, mas também em definições de parâmetros. O último é usado na seguinte função, que calcula a soma dos componentes de uma tupla:

let addComponents = ((x, y)) => x + y;
let tuple = (3, 4);
addComponents(tuple); /* 7 */

Os parênteses duplos em torno de x e y indicam que addComponents é uma função com um único parâmetro, uma tupla cujos componentes são x e y. Não é uma função com os dois parâmetros x e y. O seu tipo é:

addComponents: ((int, int)) => int

Quando se trata de anotações de tipo, você pode anotar os componentes:

# let addComponents = ((x: int, y: int)) => x + y;
let addComponents: ((int, int)) => int = <fun>;

Ou você pode anotar todo o parâmetro:

# let addComponents = ((x, y): (int, int)) => x + y;
let addComponents: ((int, int)) => int = <fun>;

7. Parâmetros rotulados

Até agora, nós só usamos parâmetros posicionais: a posição real de um parâmetro na chamada determina a qual parâmetro ele está vinculado.

Mas o ReasonML também suporta parâmetros rotulados. Aqui, os rótulos são usados para associar parâmetros reais aos parâmetros formais.

Como exemplo, vamos examinar uma versão do add que usa parâmetros nomeados:

let add = (~x, ~y) => x + y;
add(~x=7, ~y=9); /* 16 */

Nesta definição de função, usamos o mesmo nome para o rótulo ~x e o parâmetro x. Você também pode usar nomes separados, ex: ~x para o rótulo e op1 para o parâmetro:

let add = (~x as op1, ~y as op2) => op1 + op2;

E na chamada, você pode abreviar ~x = x como apenas ~x:

let x = 7;
let y = 9;
add(~x, ~y);

Uma boa funcionalidade dos rótulos é que você pode mencionar os parâmetros rotulados em qualquer ordem:

# add(~x=3, ~y=4);
- : int = 7
# add(~y=4, ~x=3);
- : int = 7

Há um problema infeliz que temos que mencionar sobre os parâmetros rotulados em qualquer ordem: os tipos de função só são compatíveis se os rótulos forem mencionados na mesma ordem.

Considere as seguintes três funções:

let add = (~x, ~y) => x + y;
let addxy = (add: ((~x: int, ~y: int) => int)) => add(5, 2);
let addyx = (add: ((~y: int, ~x: int) => int)) => add(5, 2);

addyx funciona corretamente com:

# addxy(add);
- : int = 7

No entanto, com addyx, recebemos um erro, porque os rótulos estão na ordem errada:

# addyx(add);
Error: This expression has type
(~x: int, ~y: int) => int
but an expression was expected of type
(~y: int, ~x: int) => int

8. Parâmetros opcionais

Em ReasonML, somente os parâmetros rotulados podem ser opcionais. No código a seguir, x e y são opcionais.

let add = (~x=?, ~y=?, ()) =>
switch (x, y) {
| (Some(x'), Some(y')) => x' + y'
| (Some(x'), None) => x'
| (None, Some(y')) => y'
| (None, None) => 0
};
add(~x=7, ~y=2, ()); /* 9 */
add(~x=7, ()); /* 7 */
add(~y=2, ()); /* 2 */
add(()); /* 0 */
add(); /* 0 */

Vamos examinar o que este código relativamente complicado faz.

Por que o () como o último parâmetro? Isso é explicado na próxima seção.

O que faz a expressão switch? x e y foram declarados opcionais via =?. Como conseqüência, ambos têm o tipo option(int). option é uma variante. Os detalhes serão explicados em um próximo artigo. Por enquanto, vou dar uma breve introdução. A definição de option é:

type option('a) = None | Some('a);

option é usada da seguinte maneira ao chamar add():

  • Se você omitir o parâmetro ~x, então x será vinculado a None.
  • Se você fornecer o valor 123 para ~x, então, o x será vinculado a Some(123).

Em outras palavras, option envolve os valores em uma estrutura de dados e a expressão switch extrai os valores dessa estrutura.

Por que add tem um parâmetro de tipo unit (um parâmetro vazio, se você quiser) no final?

let add = (~x=?, ~y=?, ()) =>
···

A razão tem a ver com a aplicação parcial (o que é explicado em detalhes mais tarde). Em poucas palavras, duas coisas estão acontecendo aqui:

  • Com a aplicação parcial, se você omitir parâmetros, você cria uma função que permite preencher esses parâmetros restantes.
  • Com parâmetros opcionais, se você omitir parâmetros, eles devem estar vinculados aos seus valores padrões.

Para resolver esse conflito, o ReasonML preenche valores padrões para todos os parâmetros opcionais ausentes quando ele encontra o primeiro parâmetro de posição. Antes de encontrar um parâmetro de posição, ele ainda aguarda os parâmetros opcionais em falta. Ou seja, você sempre precisa de um parâmetro de posição para disparar a chamada. Como add() não tem um, nós adicionamos um vazio. O padrão () força esse parâmetro a ser (), por meio da desestruturação.

Outra razão para o parâmetro vazio é que, caso contrário, não poderíamos ativar os dois padrões, porque add() é o mesmo que add(()).

A vantagem desta abordagem, ligeiramente estranha, é que você obtenha o melhor dos dois mundos: aplicação parcial e parâmetros opcionais.

Quando você anotar parâmetros opcionais, eles devem ter todos os tipos option(...):

let add = (~x: option(int)=?, ~y: option(int)=?, ()) =>
···

A assinatura do tipo de add é:

(~x: int=?, ~y: int=?, unit) => int

Infelizmente, a definição seria diferente da assinatura do tipo neste caso. O raciocínio é que queremos distinguir duas coisas:

  • Tipos externos: Você normalmente chama add com ints e não com valores option.
  • Tipos internos: internamente, você precisa processar os valores de option.

Na próxima seção, usamos valores padrão de parâmetros em add. Em seguida, os tipos internos são diferentes, mas os tipos externos (e, portanto, o tipo de add) são os mesmos.

O uso de parâmetros faltantes pode ser complicado:

let add = (~x=?, ~y=?, ()) =>
switch (x, y) {
| (Some(x'), Some(y')) => x' + y'
| (Some(x'), None) => x'
| (None, Some(y')) => y'
| (None, None) => 0
};

Nesse caso, tudo o que queremos é que x e y sejam zero se forem omitidos. ReasonML tem sintaxe especial para isso:

let add = (~x=0, ~y=0, ()) => x + y;

Esta nova versão do add é usada exatamente da mesma maneira que antes.

Se houver valores padrão, você anota os parâmetros da seguinte maneira:

let add = (~x: int=0, ~y: int=0, ()) => x + y;

O tipo de add é:

(~x: int=?, ~y: int=?, unit) => int

Internamente, os parâmetros opcionais são recebidos como elementos do tipo de option, sendo None ou Some(x). Até agora, você só poderia passar esses valores fornecendo ou omitindo parâmetros. Há também, uma maneira de passar esses valores diretamente. Antes de irmos para os casos de uso desse recurso, vamos tentar primeiro, através da seguinte função.

let multiply = (~x=1, ~y=1, ()) => x * y;

multiply tem dois parâmetros opcionais. Vamos começar fornecendo ~x e omitindo ~y , através do elemento option.

# multiply(~x = ?Some(14), ~y = ?None, ());
- : int = 14

A sintaxe para passar valores option é:

~label = ?expression

Se expression é uma variável que o rótulo é label , você pode abreviar usando a sintaxe a seguir:

~foo = ?foo
~foo?

Então, qual é o caso de uso? É uma função encaminhando um parâmetro opcional para o parâmetro opcional de outra função. Dessa forma, ele pode confiar no valor padrão do parâmetro dessa função e não precisa definir um próprio.

Vejamos um exemplo: A função a seguir square possui um parâmetro opcional, que é transmitido para os dois parâmetros opcionais de multiply:

let square = (~x=?, ()) => multiply(~x?, ~y=?x, ());

square não precisa especificar um valor padrão de parâmetro, ele pode usar os valores padrões de multiply.

9. Aplicação parcial

A aplicação parcial é um mecanismo que torna as funções mais versáteis: se você omitir um ou mais parâmetros no final de uma chamada de função f (···), f retorna uma função que mapeia os parâmetros ausentes para o resultado final de f. Ou seja, você aplica f aos seus parâmetros em várias etapas. O primeiro passo é chamado de aplicação parcial ou uma chamada parcial.

Vamos ver como isso funciona. Primeiro criamos uma função add com dois parâmetros:

# let add = (x, y) => x + y;
let add: (int, int) => int = <fun>;

Então, nós chamamos parcialmente a função binária add para criar a função unária plus5:

# let plus5 = add(5);
let plus5: (int) => int = <fun>;

Nós apenas fornecemos o primeiro parâmetro x para add. Sempre que chamamos plus5, fornecemos o segundo parâmetro de add, o y:

# plus5(2);
- : int = 7

A aplicação parcial permite que você escreva um código mais compacto. Para demonstrar como, vamos trabalhar com uma lista de números:

# let numbers = [11, 2, 8];
let numbers: list(int) = [11, 2, 8];

Em seguida, usaremos a função padrão List.map. List.map(func, myList) recebe myList, e aplica func a cada um de seus elementos, retornando uma nova lista.

Nós usamos Lista.map para executar add2 a cada elemento:

# List.map(x => add(2, x), numbers);
- : list(int) = [13, 4, 10]

Com uma aplicação parcial, podemos tornar este código mais compacto:

# List.map(add(2), numbers);
- : list(int) = [13, 4, 10]

Qual versão é melhor? Isso depende do seu gosto. A primeira versão é — sem dúvida — mais auto-descritiva, a segunda versão é mais concisa.

A aplicação parcial realmente brilha com o pipeline operator (|>) para a composição de funções (o que é explicado mais tarde).

Até agora, só vimos aplicação parcial com parâmetros posicionais, mas também funciona com parâmetros rotulados. Considere, novamente, a versão rotulada de add:

# let add = (~x, ~y) => x + y;
let add: (~x: int, ~y: int) => int = <fun>;

Se chamamos add com apenas o primeiro parâmetro rotulado, obtemos uma função que mapeia o segundo parâmetro para o resultado:

# add(~x=4);
- : (~y: int) => int = <fun>

Fornecer apenas o segundo parâmetro rotulado funciona de forma análoga.

# add(~y=4);
- : (~x: int) => int = <fun>

Ou seja, os rótulos não impõem uma ordem aqui. Isso significa que a aplicação parcial é mais versátil com rótulos, porque você pode aplicar parcialmente qualquer parâmetro rotulado, e não apenas o último.

E os parâmetros opcionais? A seguinte versão de add tem apenas parâmetros opcionais:

# let add = (~x=0, ~y=0, ()) => x + y;
let add: (~x: int=?, ~y: int=?, unit) => int = <fun>;

Se você mencionar apenas o rótulo ~x ou apenas o rótulo ~y, a aplicação parcial funciona como antes (com a adição do parâmetro de posição do tipe unit):

# add(~x=3);
- : (~y: int=?, unit) => int = <fun>
# add(~y=3);
- : (~x: int=?, unit) => int = <fun>

No entanto, se você fornecer o parâmetro posicional, você não estará aplicando parcialmente. Os valores padrões são preenchidos imediatamente:

# add(~x=3, ());
- : int = 3
# add(~y=3, ());
- : int = 3

Mesmo se você tomar uma ou duas etapas intermediárias, o () sempre é necessário para ativar a chamada real da função. Um passo intermediário seria o seguinte.

# let plus5 = add(~x=5);
let plus5: (~y: int=?, unit) => int = <fun>;
# plus5(());
- : int = 5

Duas etapas intermediárias:

# let plus5 = add(~x=5);
let plus5: (~y: int=?, unit) => int = <fun>;
# let result8 = plus5(~y=3);
let result8: (unit) => int = <fun>;
# result8(());
- : int = 8

Currying é uma técnica para implementar aplicação parcial para parâmetros posicionais. Fazer o currying de uma função significa transformá-la de uma função com uma aridade de 1 ou mais para uma série de chamadas de função unária.

Por exemplo, a função binária add:

let add = (x, y) => x + y;

Para transformá-la utilizando currying, fazemos:

let add = x => y => x + y;

Agora, precisamos chamar add da seguinte forma:

# add(3)(1);
- : int = 4

O que ganhamos? A aplicação parcial é fácil agora:

# let plus4 = add(4);
let plus4: (int) => int = <fun>;
# plus4(7);
- : int = 11

E agora a surpresa: todas as funções no ReasonML são automaticamente convertidas para suportar currying. É assim que aplicação parcial funciona. Você pode ver isso ao observar o tipo da função add transformada com currying:

# let add = x => y => x + y;
let add: (int, int) => int = <fun>;

Em outras palavras: add(x, y) é o mesmo que add(x)(y) e os seguintes tipos são equivalentes:

(int, int) => int
int => int => int

Vamos concluir com uma função que faça o currying de funções binárias. Dado que o currying de funções que já são curry não tem sentido, nós faremos o currying de uma função cujo único parâmetro é um par.

let curry2 = (f: (('a, 'b)) => 'c) => x => y => f((x, y));

Vamos usar curry2 com uma versão unária de add:

# let add = ((x, y)) => x + y;
let add: ((int, int)) => int = <fun>;
# curry2(add);
- : (int, int) => int = <fun>

O tipo no final nos diz que criamos uma função binária com sucesso.

10. O operador de aplicação reversa (|>)

O operador |> é chamado de operador de aplicação reversa, ou pipeline operator. Ele permite encadear chamadas de funções: x |> f é o mesmo que f(x). Isso pode não parecer muito, mas é bastante útil ao combinar chamadas de função.

Vamos começar com um exemplo simples. Dadas as duas funções a seguir.

let times2 = (x: int) => x * 2;
let twice = (s: string) => s ++ s;

Se os usarmos com chamadas de função tradicionais, obtemos:

# twice(string_of_int(times2(4)));
- : string = "88"

Primeiro, aplicamos times2 a 4, then string_of_int (uma função da biblioteca padrão) para o resultado, etc. O pipeline operator nos permite escrever um código mais próximo da descrição que acabei de dar:

let result = 4 |> times2 |> string_of_int |> twice;

Com dados e currying mais complexos, nós temos um estilo que lembra as chamadas de métodos em cadeia da programação orientada a objetos.

Por exemplo, o código a seguir funciona com uma lista de ints:

[4, 2, 1, 3, 5]
|> List.map(x => x + 1)
|> List.filter(x => x < 5)
|> List.sort(compare);

Essas funções serão explicadas em detalhes em um próximo artigo. Por enquanto, é suficiente ter uma idéia aproximada de como eles funcionam.

Os três passos computacionais são:

# let l0 = [4, 2, 1, 3, 5];
let l0: list(int) = [4, 2, 1, 3, 5];
# let l1 = List.map(x => x + 1, l0);
let l1: list(int) = [5, 3, 2, 4, 6];
# let l2 = List.filter(x => x < 5, l1);
let l2: list(int) = [3, 2, 4];
# let l3 = List.sort(compare, l2);
let l3: list(int) = [2, 3, 4];

Nós vemos que em todas essas funções, o parâmetro primário é o último. Quando estamos encadeando funções, primeiro preenchemos os parâmetros secundários através de uma aplicação parcial, criando uma função. Em seguida, o pipeline operator preenche o parâmetro primário, chamando essa função.

O parâmetro primário é semelhante a this ou self em linguagens de programação orientadas a objetos.

Quando você usa uma aplicação parcial para criar os operandos para o pipeline opereator, há um erro que é fácil de fazer. Veja se você pode detectar esse erro no seguinte código.

let conc = (x, y) => y ++ x;"a"
|> conc("b")
|> conc("c")
|> print_string();
/* Error: This expression has type unit
but an expression was expected of type string */

O problema é que estamos tentando aplicar parcialmente zero parâmetros. Isso não funciona, porque print_string() é o mesmo que print_string(()). E o único parâmetro de print_string é de tipo string (não do tipo unit).

Se você omitir os parênteses após print_string, tudo funciona conforme o previsto:

"a"
|> conc("b")
|> conc("c")
|> print_string;
/* Saída: abc */

11. Dicas para criar assinaturas de função

Estas são algumas dicas para projetar o tipo de assinaturas de funções:

  • Se uma função tiver um único parâmetro primário, transforme-o em um parâmetro de posição e coloque-o no final. Isso suporta o pipeline operatorr para a composição da função.
  • Algumas funções têm múltiplos parâmetros primários que são semelhantes. Transforme estes em vários parâmetros posicionais no final. Um exemplo seria uma função que concatena duas listas em uma única lista. Nesse caso, ambos os parâmetros posicionais são listas.
  • Todos os outros parâmetros devem ser rotulados.
  • Se houver dois ou mais parâmetros principais que sejam diferentes, todos eles devem ser rotulados.
  • Se uma função tiver apenas um único parâmetro, ele tende a não ser rotulado, mesmo que não seja estritamente primária.

A idéia por trás dessas regras é tornar o código tão autodescritivo quanto possível: o parâmetro primário (ou único) é descrito pelo nome da função, os parâmetros restantes são descritos por seus rótulos.

Assim que uma função possui mais de um parâmetro de posição, geralmente fica difícil dizer o que cada parâmetro faz. Compare, por exemplo, as duas chamadas de função a seguir. O segundo é muito mais fácil de entender.

blit(bytes, 0, bytes, 10, 10);
blit(~src=bytes, ~src_pos=0, ~dst=bytes, ~dst_pos=10, ~len=10);

Também gosto de parâmetros opcionais, pois permitem que você adicione mais parâmetros às funções sem quebrar as chamadas existentes. Isso ajuda com a evolução das APIs.

Fonte desta seção: “Suggestions for labeling” no Manual do OCaml.

12. Abreviação de funções unárias com switch

O ReasonML fornece uma abreviação para funções unárias que imediatamente alternam seus parâmetros. Pegue, por exemplo, a seguinte função.

let divTuple = (tuple) =>
switch tuple {
| (_, 0) => (-1)
| (x, y) => x / y
};

A função é usada da seguinte maneira:

# divTuple((9, 3));
- : int = 3
# divTuple((9, 0));
- : int = -1

Se você usar o operador fun para definir divTuple, o código fica mais curto:

let divTuple =
fun
| (_, 0) => (-1)
| (x, y) => x / y;

13. Alerta: avançado

Todas as seções abaixo abordam tópicos avançados.

14. Operadores

Uma funcionalidade bacana do ReasonML são que os operadores são apenas funções. Você pode usá-los como funções se você colocá-los entre parênteses:

# (+)(7, 1);
- : int = 8

E você pode definir seus próprios operadores:

# let (+++) = (s, t) => s ++ " " ++ t;
let ( +++ ): (string, string) => string = <fun>;
# "hello" +++ "world";
- : string = "hello world"

Ao colocar um operador entre parênteses, você também pode procurar facilmente seu tipo:

# (++);
- : (string, string) => string = <fun>

Existem dois tipos de operadores: operadores infixo (entre dois operandos) e operadores de prefixo (antes de operandos individuais).

Os seguintes caracteres de operador podem ser usados para ambos os tipos de operadores:

! $ % & * + - . / : < = > ? @ ^ | ~

Operadores Infixo:

Image for post
Image for post

Além disso, as seguintes palavras-chave são operadores infixos:

* + - -. == != < > || && mod land lor lxor lsl lsr asr

Operadores de prefixos:

Image for post
Image for post

Além disso, as seguintes palavras-chave são operadores de prefixos:

- -.

Fonte desta seção: “Prefix and infix symbols” no Manual do OCaml.

As tabelas a seguir lista os operadores e suas associações. Quanto mais alto um operador, maior é a precedência. Por exemplo, * tem uma precedência superior a +.

Image for post
Image for post

Legenda:

  • op··· significa operador seguido por outros caracteres do operador.
  • applications: aplicações de função, aplicações de construtor, aplicações de tag

Fonte desta seção: “Expressions” no Manual do OCaml.

A associatividade é importante sempre que um operador não é comutativo. Com um operador comutativo, a ordem dos operandos não importa. Por exemplo, plus (+) é comutativo. No entanto, minus (-) não é comutativo.

A associatividade na esquerda significa que as operações são agrupadas a partir da esquerda. Então, as duas expressões a seguir são equivalentes:

x op y op z
(x op y) op z

minus (-) é associativo à esquerda:

# 3 - 2 - 1;
- : int = 0

A associatividade na direta significa que as operações são agrupadas da direita. Então, as duas expressões a seguir são equivalentes:

x op y op z
x op (y op z)

Podemos definir nosso próprio operador negativo associado à direita. De acordo com a tabela de operadores, se ele começa com um símbolo @, é automaticamente associativo à direita:

let (@-) = (x, y) => x - y;

Se o usarmos, obtemos um resultado diferente do normal minur (-):

# 3 @- 2 @- 1;
- : int = 2

15. Funções polimórficas

Relembrando a definição de polimorfismo: fazer a mesma operação funcionar para vários tipos. Existem várias maneiras pelas quais o polimorfismo pode ser alcançado. As linguagens OOP conseguem através da subclasse. A sobrecarga é outro tipo popular de polimorfismo.

O ReasonML suporta polimorfismo paramétrico: ao invés de usar um tipo concreto, como int para um parâmetro ou um resultado, você usa uma variável de tipo. Se o tipo de um parâmetro for uma variável de tipo, os valores de qualquer tipo são aceitos (caso você esteja interessado: tais variáveis de tipo são universalmente quantificadas). As variáveis de tipo podem ser vistas como parâmetros do tipo de função; daí o nome do polimorfismo paramétrico.

Uma função que usa variáveis de tipo é chamada de função genérica.

Por exemplo, id() é a função de identidade que simplesmente retorna seu parâmetro:

# let id = x => x;
let id: ('a) => 'a = <fun>;

O tipo que o ReasonML infere ao id é interessante: não pode detectar um tipo para x, então ele usa a variável de tipo ' para indicar “qualquer tipo”. Todos os tipos cujos nomes começam com apóstrofos são variáveis de tipo. O ReasonML também infere que o tipo de retorno do id é o mesmo que o tipo de seu parâmetro. Isso é uma informação útil e ajuda a inferir o tipo de resultado do id.

Em outras palavras: id é genérico e funciona com qualquer tipo.

# id(123);
- : int = 123
# id("abc");
- : string = "abc"

Vamos ver outro exemplo: uma função genérica first para acessar o primeiro componente de um par (uma 2-tupla).

# let first = ((x, y)) => x;
let first: (('a, 'b)) => 'a = <fun>;

Primeiro usamos a desestruturação para acessar o primeiro componente dessa tupla. A inferência de tipo nos diz que o tipo de retorno é o mesmo que o tipo do primeiro componente.

Podemos usar um sublinhado para indicar que não estamos interessados no segundo componente:

# let first = ((x, _)) => x;
let first: (('a, 'b)) => 'a = <fun>;

E se anotarmos o componente, ficaria dessa forma:

# let first = ((x: 'a, _)) => x;
let first: (('a, 'b)) => 'a = <fun>;

Como uma prévia rápida, estou mostrando a assinatura de outra função que vou explicar corretamente em uma próxima postagem:

ListLabels.map: (~f: ('a) => 'b, list('a)) => list('b)

Observe como a sobrecarga e o polimorfismo paramétrico são diferentes:

  • A sobrecarga fornece implementações diferentes para a mesma operação. Por exemplo, algumas linguagens de programação permitem usar + para aritmética, concatenação de strings e/ou concatenação de arrays.
  • O polimorfismo paramétrico especifica uma implementação única que funciona com vários tipos.

16. ReasonML não suporta funções variadicas

O ReasonML não suporta funções variadicas (por exemplo, via varargs). Ou seja, você não pode definir uma função que calcula a soma de um número arbitrário de parâmetros:

let sum = (x0, ···, xn) => x0 + ··· + xn;

Ao invés disso, você é obrigado a definir uma função para cada aridade:

let sum2(a: int, b: int) = a + b;
let sum3(a: int, b: int, c: int) = a + b + c;
let sum4(a: int, b: int, c: int, d: int) = a + b + c + d;

Você viu uma técnica similar com curry, onde não conseguimos definir uma função variadica curry() e tivemos que ir com uma função binária curry2(). Você ocasionalmente encontra isso em bibliotecas. Uma alternativa a esta técnica é usar um array de ints.

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