O programador

Diversão com o C#

Ted Neward

Ted NewardAprender um novo idioma pode ajudar os desenvolvedores a ter novas ideias e obter novas abordagens para criar um código em outras linguagens, como o C#. Minha preferência pessoal é o F#, porque eu sou um MVP do F# agora. Embora eu tenha mencionado brevemente a programação pessoal na coluna anterior (bit.ly/1lPaLNr), desejo analisar agora uma nova linguagem.

Ao fazer isso, provavelmente o código precisará ser escrito em C# (farei isso na próxima parte). Mas ainda pode ser útil codificar em F# por três motivos:

  1. O F# pode algumas vezes resolver problemas como esse mais facilmente do que o C#.
  2. Pensando sobre um problema em uma linguagem diferente, pode frequentemente ajudar a esclarecer a solução antes de recriar em C#.
  3. F# é uma linguagem .NET como seu primo C#. Portanto, você pode resolver em F#, compilar em um assembly .NET e apenas chamar no C#. (Dependendo da complexidade do cálculo/algoritmo, pode ser realmente a solução mais adequada.)

Observe o problema

Considere um problema simples para este tipo de solução. Imagine que você está trabalhando no Speedy, um aplicativo para gerenciar finanças pessoais. Como parte do aplicativo, você precisa “reconciliar” as transações encontradas online com as transações que um usuário inseriu no aplicativo. O objetivo aqui é analisar duas listas de dados praticamente idênticos e corresponder os elementos iguais. O que você faz com esses elementos não correspondentes ainda não foi especificado, mas você precisa identificá-los.

Há alguns anos atrás, eu fiz alguns contratos para a empresa “intuitiva” que criou o aplicativo para computador de gerenciamento da sua atividade bancária mais popular da época. Isso foi um problema real que precisei resolver. Era especificamente para verificar o extrato bancário depois de baixar as transações de um usuário, conforme informado pelo banco. Eu precisei reconciliar essas transações online com aquelas que o usuário já tinha inserido no aplicativo e perguntar ao usuário sobre qualquer transação que não correspondia.

Cada transação consiste de um valor, uma data de transação e um “comentário” descritivo. Aqui está o problema: nem sempre as datas correspondiam e nem os comentários.

Isso significa que o único dado real que poderia utilizar para comparar era o valor da transação. Felizmente, é muito raro dentro de um determinado mês que duas transações tenham valores absolutamente idênticos. Portanto, essa é uma solução “boa o suficiente”. Eu voltarei e confirmarei que de fato há uma correspondência legítima. Apenas para complicar as coisas, as duas listas fornecidas não têm uma correspondência no comprimento.

Uma solução F#ing

Há princípios sobre as linguagens funcionais que dominam a forma de “pensar funcionalmente”. Neste caso, um dos primeiros é que eles preferem recursão em vez de iteração. Em outras palavras, enquanto o desenvolvedor com treinamento clássico desejará imediatamente destacar um par de aninhados para loops, o programador funcional desejará um recursivo.

Aqui, pegarei a lista de transações locais e a lista de transações remotas. Analisarei o primeiro elemento de cada lista. Se corresponderem, irei retirar as duas de suas respectivas listas, agrupá-las na lista de resultados e recursivamente irei chamar novamente o lembrete das listas locais e remotas. Observe o tipo de definições para as quais estou trabalhando:

type Transaction = { amount : float32; date : System.DateTime; comment : string } type Register = | RegEntry of Transaction * Transaction

Em termos simples, estou definindo dois tipos. Um é um tipo de registro, que é realmente um objeto com algumas anotações de objeto tradicional. O outro é um tipo de união discriminada, que é realmente um gráfico de objeto/classe disfarçado. Não irei me aprofundar na sintaxe do F# aqui. Há muitos outros recursos para isso, incluindo o meu livro, “Professional F# 2.0” (Wrox, 2010).

É suficiente dizer que são tipos de entrada e tipos de saída, respectivamente. O motivo pelo qual eu escolhi uma união discriminada para o resultado logo se tornará claro. Dadas essas duas definições de tipo, é muito fácil definir o esqueleto externo do que eu desejo que esta função se pareça:

let reconcile (local : Transaction list) (remote : Transaction list) : Register list = []

Lembre-se, no jargão do F#, os descritores de tipo são colocados depois do nome. Portanto, isso está declarando uma função que tem duas listas de Transação e retorna uma lista de itens de Registro. Conforme escrito, esboça para retornar uma lista vazia (“[]”). Isso é bom, porque agora eu posso esboçar algumas funções para testar—estilo Test-Driven Development (TDD)—em um aplicativo de console F# comum.

Eu posso e devo redigir em uma estrutura de teste de unidade no momento, mas posso realizar basicamente a mesma coisa usando System.Diagnostics.Debug.Assert e aninhando localmente funções dentro do principal. Outros podem preferir trabalhar com o REPL do F#, no Visual Studio ou na linha de comando, conforme mostrado na Figura 1.

Figura 1 Criar um algoritmo de console com o REPL do F#

[<EntryPoint>] let main argv = let test1 = let local = [ { amount = 20.00f; date = System.DateTime.Now; comment = "ATM Withdrawal" } ] let remote = [ { amount = 20.00f; date = System.DateTime.Now; comment = "ATM Withdrawal" } ] let register = reconcile local remote Debug.Assert(register.Length = 1, "Matches should have come back with one item") let test2 = let local = [ { amount = 20.00f; date = System.DateTime.Now; comment = "ATM Withdrawal" }; { amount = 40.00f; date = System.DateTime.Now; comment = "ATM Withdrawal" } ] let remote = [ { amount = 20.00f; date = System.DateTime.Now; comment = "ATM Withdrawal" } ] let register = reconcile local remote Debug.Assert(register.Length = 1, "Register should have come back with one item") 0 // Return an integer exit code

Como eu tenho uma armação de teste básico pronta, irei atacar a solução recursiva, como você pode ver na Figura 2.

Figura 2 Use o padrão do F# correspondente para uma solução recursiva

let reconcile (local : Transaction list) (remote : Transaction list) : Register list = let rec reconcileInternal outputSoFar local remote = match (local, remote) with | [], _ -> outputSoFar | _, [] -> outputSoFar | loc :: locTail, rem :: remTail -> match (loc.amount, rem.amount) with | (locAmt, remAmt) when locAmt = remAmt -> reconcileInternal (RegEntry(loc, rem) :: outputSoFar) locTail remTail | (locAmt, remAmt) when locAmt < remAmt -> reconcileInternal outputSoFar locTail remTail | (locAmt, remAmt) when remAmt > locAmt -> reconcileInternal outputSoFar locTail remTail | (_, _) -> failwith("How is this possible?") reconcileInternal [] local remote

Como você irá observar, isto usa muito a correspondência do padrão do F#. Isso é conceitualmente parecido com o bloco de troca do C# (da mesma forma que um gatinho é conceitualmente parecido com um tigre dente de sabre). Primeiro, eu defino uma função recursiva local (rec) que é basicamente a mesma assinatura que a função externa. Há um parâmetro adicional para transportar os resultados correspondentes até o momento.

Dentro dele, o primeiro bloco correspondente examina as listas locais e remota. A primeira cláusula de correspondência ( [],_) informa que se a lista local está vazia, não me importa o que a lista remota é (o sublinhado é um curinga), pois eu terminei. Portanto, basta retornar os resultados obtidos até o momento. O mesmo ocorre para a segunda cláusula de correspondência ( _, []).

A dificuldade em tudo isso ocorre na última cláusula de correspondência. Isso extrai o cabeçalho da lista local e o vincula ao valor loc, coloca o resto da lista no locTail, faz o mesmo para o remoto em rem e remTail, e corresponde novamente. Desta vez, eu extraio os campos de valor de cada um dos dois itens obtidos das listas e os vinculo nas variáveis locais locAmt e remAmt.

Para cada uma dessas cláusulas de correspondência, eu recursivamente invocarei reconcile­Internal. A principal diferença é o que eu faço com a lista outputSoFar antes de fazer o recurso. Se locAmt e remAmt são iguais, é uma correspondência, portanto, eu insiro um novo RegEntry na lista outputSoFar antes de fazer o recurso. Em qualquer outro caso, eu apenas ignoro e faço o recurso. O resultado será uma lista de itens RegEntry e isso é retornado para o chamador.

Aumentar a ideia

Vamos supor que eu possa apenas ignorar estes itens não correspondentes. Eu preciso colocar um item na lista resultante que informa que havia uma transação local não correspondente ou uma transação remota não correspondente. O algoritmo principal ainda se mantém, eu apenas adiciono novos itens na união discriminada do Registro para manter cada uma dessas possibilidades, e as anexo na lista antes de fazer o recurso, conforme mostrado na Figura 3.

Figura 3 Adicionar novos itens para registrar

type Register = | RegEntry of Transaction * Transaction | MissingRemote of Transaction | MissingLocal of Transaction let reconcile (local : Transaction list) (remote : Transaction list) : Register list = let rec reconcileInternal outputSoFar local remote = match (local, remote) with | [], _ | _, [] -> outputSoFar | loc :: locTail, rem :: remTail -> match (loc.amount, rem.amount) with | (locAmt, remAmt) when locAmt = remAmt -> reconcileInternal (RegEntry(loc, rem) :: outputSoFar) locTail remTail | (locAmt, remAmt) when locAmt < remAmt -> reconcileInternal (MissingRemote(loc) :: outputSoFar) locTail remote | (locAmt, remAmt) when locAmt > remAmt -> reconcileInternal (MissingLocal(rem) :: outputSoFar) local remTail | _ -> failwith "How is this possible?" reconcileInternal [] local remote

Agora os resultados serão uma lista completa, com as entradas MissingLocal ou MissingRemote para cada Transação que não tem um par correspondente. Na realidade, isso não é totalmente verdadeiro. Se as duas listas não corresponderem no comprimento, como meu caso test2 anterior, os itens restantes não terão entradas “Ausentes”.

Considerando o F# como a linguagem “conceitual” em vez do C# e usando os princípios de programação funcional, esta se torna uma solução muito rápida. O F# usa uma inferência de tipo extensa, portanto, em muitos casos durante a retirada do código, eu não preciso determinar os tipos atuais para os parâmetros e retornar antecipadamente. As funções recursivas em F# frequentemente precisarão de um tipo de anotação para definir o tipo de retorno. Eu terminei sem ele porque podia inferir do tipo de retorno fornecido na função de fechamento externa.

Em alguns casos, eu podia apenas compilar em um assembly e entregar para os desenvolvedores do C#. Para muitas lojas, infelizmente, isso não irá decolar. Portanto, na próxima vez irei converter para C#. O chefe nunca saberá que este código realmente começou sua vida como um código do F#.

Boa codificação.

Ted Neward é o CTO na iTrellis, uma empresa de serviços de consultoria. Ele já escreveu mais de 100 artigos e é autor e coautor de dezenas de livros, incluindo “Professional F# 2.0” (Wrox, 2010). Ele é um MVP de C# e participa como palestrante em conferências em todo o mundo. Ele consulta e orienta regularmente—entre em contato com ele el ted@tedneward.com ou ted@itrellis.com se você estiver interessado em tê-lo na sua equipe e leia seu blog em blogs.tedneward.com.

Agradecemos ao seguinte especialista técnico da Microsoft pela revisão deste artigo: Lincoln Atkinson