Pensamento funcional. Parte 10

Você pode imaginar que este é o décimo do ciclo! Embora a narrativa tenha se concentrado anteriormente em um estilo puramente funcional, às vezes é conveniente mudar para um estilo orientado a objetos. E um dos principais recursos de um estilo orientado a objetos é a capacidade de anexar funções a uma classe e acessar a classe através de um ponto para obter o comportamento desejado.






No F #, isso é possível com um recurso chamado "extensões de tipo". Qualquer tipo de F #, não apenas uma classe, pode ter funções anexadas.


Aqui está um exemplo de anexar uma função a um tipo de registro.


module Person = type T = {First:string; Last:string} with // -,     member this.FullName = this.First + " " + this.Last //  let create first last = {First=first; Last=last} let person = Person.create "John" "Doe" let fullname = person.FullName 

Pontos-chave a serem observados:


  • A palavra with chave with indica o início de uma lista de membros.
  • A palavra-chave member indica que a função é um membro (ou seja, um método)
  • A palavra this é o rótulo do objeto no qual esse método é chamado (também chamado de "auto-identificador"). Essa palavra é o prefixo do nome da função e, dentro da função, você pode usá-la para se referir à instância atual. Não há requisitos para palavras usadas como auto-identificador, basta que elas sejam estáveis. Você pode usar this , self , me ou qualquer outra palavra que geralmente é usada como referência a si mesmo.

Não há necessidade de adicionar um membro junto com uma declaração de tipo, você sempre pode adicioná-lo posteriormente no mesmo módulo:


 module Person = type T = {First:string; Last:string} with // ,     member this.FullName = this.First + " " + this.Last //  let create first last = {First=first; Last=last} //  ,   type T with member this.SortableName = this.Last + ", " + this.First let person = Person.create "John" "Doe" let fullname = person.FullName let sortableName = person.SortableName 

Estes exemplos demonstram a chamada para "extensões intrínsecas". Eles são compilados em um tipo e estarão disponíveis onde quer que o tipo seja usado. Eles também serão mostrados ao usar a reflexão.


As extensões internas até permitem dividir uma definição de tipo em vários arquivos, desde que todos os componentes usem o mesmo espaço para nome e compilem em um assembly. Como nas classes parciais em C #, isso pode ser útil para separar o código gerado e o manuscrito.


Extensões opcionais


Uma alternativa é adicionar um membro adicional de um módulo completamente diferente. Eles são chamados de "extensões opcionais". Eles não são compilados dentro da classe e exigem um módulo de escopo diferente para trabalhar com eles (esse comportamento é semelhante aos métodos de extensão do C #).


Por exemplo, deixe um tipo de Person ser definido:


 module Person = type T = {First:string; Last:string} with // ,     member this.FullName = this.First + " " + this.Last //  let create first last = {First=first; Last=last} //   ,   type T with member this.SortableName = this.Last + ", " + this.First 

O exemplo abaixo demonstra como adicionar a extensão UppercaseName a ele em outro módulo:


 //    module PersonExtensions = type Person.T with member this.UppercaseName = this.FullName.ToUpper() 

Agora você pode tentar esta extensão:


 let person = Person.create "John" "Doe" let uppercaseName = person.UppercaseName 

Ops, ocorreu um erro. Isso aconteceu porque PersonExtensions não PersonExtensions no escopo. Como em C #, para usar qualquer extensão, você precisa inseri-las no escopo.


Depois de fazer isso, ele funcionará:


 //    ! open PersonExtensions let person = Person.create "John" "Doe" let uppercaseName = person.UppercaseName 

Extensões de tipo de sistema


Você também pode estender os tipos das bibliotecas .NET. Mas deve-se ter em mente que, ao expandir um tipo, você precisa usar seu nome real, e não um apelido.


Por exemplo, se você tentar expandir int , nada funcionará, porque int não int um nome válido para o tipo:


 type int with member this.IsEven = this % 2 = 0 

Em vez disso, use System.Int32 :


 type System.Int32 with member this.IsEven = this % 2 = 0 let i = 20 if i.IsEven then printfn "'%i' is even" i 

Membros estáticos


Você pode criar funções de membro estáticas usando:


  • adicionar static
  • remova this tag

 module Person = type T = {First:string; Last:string} with // ,     member this.FullName = this.First + " " + this.Last //   static member Create first last = {First=first; Last=last} let person = Person.T.Create "John" "Doe" let fullname = person.FullName 

Você pode criar membros estáticos para tipos de sistema:


 type System.Int32 with static member IsOdd x = x % 2 = 1 type System.Double with static member Pi = 3.141 let result = System.Int32.IsOdd 20 let pi = System.Double.Pi 

Anexando recursos existentes


Um padrão muito comum é o anexo de funções independentes existentes a um tipo. Ele oferece várias vantagens:


  • Durante o desenvolvimento, você pode declarar funções independentes que fazem referência a outras funções independentes. Isso simplificará o desenvolvimento, pois a inferência de tipo funciona muito melhor com um estilo funcional do que com um estilo orientado a objetos ("ponto a ponto").
  • Mas algumas funções principais podem ser anexadas a um tipo. Isso permite que os usuários escolham qual dos estilos usar - funcional ou orientado a objetos.

Um exemplo dessa solução é uma função da biblioteca F #, que calcula o comprimento da lista. Você pode usar uma função independente do módulo List ou chamá-la como um método de instância.


 let list = [1..10] //   let len1 = List.length list // -  let len2 = list.Length 

No exemplo a seguir, o tipo inicialmente não possui nenhum membro, várias funções são definidas e, finalmente, a função fullName é anexada ao tipo.


 module Person = // ,     type T = {First:string; Last:string} //  let create first last = {First=first; Last=last} //   let fullName {First=first; Last=last} = first + " " + last //       type T with member this.FullName = fullName this let person = Person.create "John" "Doe" let fullname = Person.fullName person //  let fullname2 = person.FullName //  

A função fullName possui um parâmetro, person . O membro anexado recebe o parâmetro do auto-link.


Adicionando funções existentes com vários parâmetros


Há outro recurso interessante. Se uma função definida anteriormente usa vários parâmetros, quando você a anexa a um tipo, não é necessário listar todos esses parâmetros novamente. Basta especificar this parâmetro primeiro.


No exemplo abaixo, a função hasSameFirstAndLastName possui três parâmetros. No entanto, ao anexar, basta mencionar apenas um!


 module Person = //    type T = {First:string; Last:string} //  let create first last = {First=first; Last=last} //   let hasSameFirstAndLastName (person:T) otherFirst otherLast = person.First = otherFirst && person.Last = otherLast //      type T with member this.HasSameFirstAndLastName = hasSameFirstAndLastName this let person = Person.create "John" "Doe" let result1 = Person.hasSameFirstAndLastName person "bob" "smith" //  let result2 = person.HasSameFirstAndLastName "bob" "smith" //  

Por que isso funciona? Dica: pense em curry e uso parcial!


Métodos de tupla


Quando temos métodos com mais de um parâmetro, você precisa tomar uma decisão:


  • podemos usar o formulário padrão (com curry), onde os parâmetros são separados por espaços e a aplicação parcial é suportada.
  • ou podemos passar todos os parâmetros de uma vez na forma de uma tupla separada por vírgulas.

A forma ao curry é mais funcional, enquanto a forma da tupla é mais orientada a objetos.


Um formulário de tupla também é usado para interagir com o F # com bibliotecas .NET padrão, portanto, você deve considerar essa abordagem com mais detalhes.


Nosso site de teste será do tipo Product com dois métodos, cada um dos quais implementado por um dos métodos descritos acima. Os TupleTotal e TupleTotal fazem o mesmo: calculam o custo total do produto para uma determinada quantidade e desconto.


 type Product = {SKU:string; Price: float} with //   member this.CurriedTotal qty discount = (this.Price * float qty) - discount //   member this.TupleTotal(qty,discount) = (this.Price * float qty) - discount 

Código do teste:


 let product = {SKU="ABC"; Price=2.0} let total1 = product.CurriedTotal 10 1.0 let total2 = product.TupleTotal(10,1.0) 

Até agora não há muita diferença.


Mas sabemos que a versão ao curry pode ser parcialmente aplicada:


 let totalFor10 = product.CurriedTotal 10 let discounts = [1.0..5.0] let totalForDifferentDiscounts = discounts |> List.map totalFor10 

Por outro lado, a versão tupla é capaz de algo que não pode ser curry, a saber:


  • Parâmetros nomeados
  • Parâmetros opcionais
  • Sobrecarga

Parâmetros nomeados com parâmetros na forma de uma tupla


A abordagem de tupla suporta parâmetros nomeados:


 let product = {SKU="ABC"; Price=2.0} let total3 = product.TupleTotal(qty=10,discount=1.0) let total4 = product.TupleTotal(discount=1.0, qty=10) 

Como você pode ver, isso permite alterar a ordem dos argumentos, especificando explicitamente os nomes.


Atenção: se apenas alguns dos parâmetros tiverem nomes, esses parâmetros sempre deverão estar no final.


Parâmetros opcionais com parâmetros na forma de uma tupla


Para métodos com parâmetros na forma de uma tupla, você pode marcar parâmetros como opcionais, usando um prefixo na forma de um ponto de interrogação na frente do nome do parâmetro.


  • Se o parâmetro estiver definido, Some value será passado para a função
  • Caso contrário, None virá

Um exemplo:


 type Product = {SKU:string; Price: float} with //   member this.TupleTotal2(qty,?discount) = let extPrice = this.Price * float qty match discount with | None -> extPrice | Some discount -> extPrice - discount 

E o teste:


 let product = {SKU="ABC"; Price=2.0} //    let total1 = product.TupleTotal2(10) //   let total2 = product.TupleTotal2(10,1.0) 

A verificação explícita de None e Some pode ser entediante, mas existe uma solução mais elegante para lidar com parâmetros opcionais.


Há uma função defaultArg que assume um nome de parâmetro como o primeiro argumento e um valor padrão como o segundo. Se o parâmetro estiver definido, o valor correspondente será retornado, caso contrário, o valor padrão.


O mesmo código usando defaulArg :


 type Product = {SKU:string; Price: float} with //   member this.TupleTotal2(qty,?discount) = let extPrice = this.Price * float qty let discount = defaultArg discount 0.0 extPrice - discount 

Sobrecarga de método


Em C #, você pode criar vários métodos com o mesmo nome que diferem em sua assinatura (por exemplo, vários tipos de parâmetros e / ou número).


Em um modelo puramente funcional, isso não faz sentido - a função funciona com um tipo específico de argumento (domínio) e um tipo específico de valor de retorno (intervalo). A mesma função não pode interagir com outro domínio e intervalo.


No entanto, o F # oferece suporte à sobrecarga de método, mas apenas para métodos (que estão anexados a tipos) e somente aqueles que são gravados em um estilo de tupla.


Aqui está um exemplo com outra variação do método TupleTotal !


 type Product = {SKU:string; Price: float} with //   member this.TupleTotal3(qty) = printfn "using non-discount method" this.Price * float qty //   member this.TupleTotal3(qty, discount) = printfn "using discount method" (this.Price * float qty) - discount 

Como regra, o compilador F # jura que existem dois métodos com o mesmo nome, mas, neste caso, isso é aceitável, porque eles são declarados em notação de tupla e suas assinaturas são diferentes. (Para deixar claro qual método está sendo chamado, adicionei pequenas mensagens para depuração)


Exemplo de uso:


 let product = {SKU="ABC"; Price=2.0} //    let total1 = product.TupleTotal3(10) //   let total2 = product.TupleTotal3(10,1.0) 

Ei! Não é tão rápido ... As desvantagens de usar métodos


Vindo de um mundo orientado a objetos, você pode ser tentado a usar métodos em qualquer lugar, porque é algo familiar. Mas você deve ter cuidado, porque eles têm várias desvantagens sérias:


  • Métodos não funcionam bem com inferência de tipo
  • Métodos não funcionam bem com funções de ordem superior

De fato, ao abusar dos métodos, você pode perder os aspectos mais poderosos e úteis da programação em F #.


Vamos ver o que eu quero dizer.


Os métodos interagem mal com a inferência de tipo


Voltemos ao exemplo com Person , no qual a mesma lógica foi implementada em uma função independente e em um método:


 module Person = //    type T = {First:string; Last:string} //  let create first last = {First=first; Last=last} //   let fullName {First=first; Last=last} = first + " " + last // - type T with member this.FullName = fullName this 

Agora vamos ver quão bem a inferência de tipo funciona com cada um dos métodos. Suponha que eu queira imprimir o nome completo de uma pessoa, depois definirei a função printFullName , que leva a person como parâmetro.


Código usando uma função independente do módulo:


 open Person //    let printFullName person = printfn "Name is %s" (fullName person) //    // val printFullName : Person.T -> unit 

Ele é compilado sem problemas e a inferência de tipo identifica corretamente o parâmetro como Person .


Agora tente a versão através do ponto:


 open Person //    " " let printFullName2 person = printfn "Name is %s" (person.FullName) 

Este código não compila, porque inferência de tipo não possui informações suficientes para determinar o tipo de parâmetro. Qualquer objeto pode implementar .FullName - isso não é suficiente para a saída.


Sim, podemos anotar uma função com um tipo de parâmetro, mas, por causa disso, todo o ponto da inferência automática de tipo é perdido.


Métodos vão mal com funções de ordem superior


Um problema semelhante surge em funções de ordem superior. Por exemplo, há uma lista de pessoas e precisamos obter uma lista de seus nomes completos.


No caso de uma função independente, a solução é trivial:


 open Person let list = [ Person.create "Andy" "Anderson"; Person.create "John" "Johnson"; Person.create "Jack" "Jackson"] //     list |> List.map fullName 

No caso de um método de objeto, você deve criar uma lambda especial em qualquer lugar:


 open Person let list = [ Person.create "Andy" "Anderson"; Person.create "John" "Johnson"; Person.create "Jack" "Jackson"] //    list |> List.map (fun p -> p.FullName) 

Mas este ainda é um exemplo bastante simples. Métodos de objetos são bastante passíveis de composição, inconvenientes na tubulação, etc.


Portanto, se você é novo na programação funcional, insisto: se puder, não use métodos, especialmente no processo de aprendizado. Eles serão uma muleta que não permitirá que você extraia o máximo benefício da programação funcional.


Recursos Adicionais


Existem muitos tutoriais para F #, incluindo materiais para quem vem com experiência em C # ou Java. Os links a seguir podem ser úteis à medida que você avança no F #:



Várias outras maneiras de começar a aprender F # também são descritas.


Finalmente, a comunidade F # é muito amigável para iniciantes. Há um bate-papo muito ativo no Slack, suportado pela F # Software Foundation, com salas para iniciantes nas quais você pode participar livremente . É altamente recomendável que você faça isso!


Não se esqueça de visitar o site da comunidade de língua russa F # ! Se você tiver alguma dúvida sobre o aprendizado de um idioma, teremos prazer em discuti-los nas salas de bate-papo:



Sobre autores de tradução


Traduzido por @kleidemos
As mudanças de tradução e editoriais foram feitas pelos esforços da comunidade de desenvolvedores de F # de língua russa . Agradecemos também a @schvepsss e @shwars pela preparação deste artigo para publicação.

Source: https://habr.com/ru/post/pt433410/


All Articles