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.