Il presente articolo è stato tradotto automaticamente.

Il programmatore al lavoro

Divertirsi con C#

Ted Neward

Ted NewardImparare una nuova lingua può aiutare gli sviluppatori a portare nuove conoscenze e nuovi approcci alla scrittura di codice in altre lingue, alla C#. Qui la mia preferenza personale è F #, perché io sono un F # MVP in questi giorni. Mentre ho brevemente accennato alla programmazione funzionale in una colonna precedente (bit.ly/1lPaLNr), voglio guardare qui una nuova lingua.

Quando si fa questo, è probabile che il codice alla fine dovranno essere scritti in c# (farò che nella prossima puntata). Ma può ancora essere utile per il codice è in F # per tre motivi:

  1. F # può a volte risolvere i problemi come questo più facilmente di c#.
  2. Pensando a un problema in un'altra lingua spesso può aiutare a chiarire la soluzione prima di riscriverla in c#.
  3. F # è un linguaggio .NET come il suo cugino c#. Quindi in teoria si potrebbe risolvere in F #, poi compilarlo in un assembly .NET e semplicemente chiamare esso da c#. (A seconda della complessità dell'algoritmo di calcolo, potrebbe effettivamente essere la soluzione più sensata.)

Esaminare il problema

Si consideri un semplice problema per questo tipo di soluzione. Immagina che stai lavorando su Speedy, un'applicazione per la gestione delle finanze personali. Come parte dell'applicazione, è necessario "conciliare" le transazioni che hai trovato online con le operazioni che un utente ha immesso in app. L'obiettivo è di lavorare attraverso due elenchi di dati per lo più identici e abbinare gli elementi identici. Cosa fare con quegli elementi ineguagliati non è ancora non specificati, ma devi catturarli.

Anni fa, ho fatto alcuni contraenti per la società "intuitiva" che fa quello che allora era il più popolare applicazione per PC per gestire il vostro bancario. Questo era un problema reale che ho dovuto lavorare lì. Esso è stato specificamente per la visualizzazione del registro di conto corrente dopo aver scaricato le transazioni dell'utente come conosciuto dalla banca. Dovevo conciliare tali transazioni online con quelli dell'utente era già entrato in app e poi chiedere all'utente sulle eventuali transazioni che non corrispondono.

Ogni transazione è costituita da un importo, una data di transazione e un "commento" descrittivo. Qui è il guaio: Le date non corrispondono sempre, e nemmeno i commenti.

Questo significa che i solo vero credibili dati che potuto confrontare erano l'importo della transazione. Fortunatamente, è piuttosto rara all'interno di un dato mese che due transazioni saranno assolutamente identiche per il penny. Si tratta quindi di una soluzione "abbastanza buona". Io tornare indietro e confermare che sono in realtà una corrispondenza legit.Giusto per complicare le cose, le due liste in arrivo non devono corrispondere in lunghezza.

Un F #ing soluzione

Ci sono principi sui linguaggi funzionali che dominano come "che funzionalmente." In questo caso, uno dei primi è che essi preferiscono la ricorsione su iterazione. In altre parole, mentre lo sviluppatore classicamente addestrato immediatamente vorrà alzarsi un paio di cicli for nidificati, il programmatore funzionale vorranno recurse.

Qui, mi prendo la lista delle transazioni e l'elenco delle transazioni remoto. Potrai attraversare il primo elemento di ogni lista. Se corrispondono, io quei due fuori i loro rispettivi elenchi di sbucciare, schiacciarle insieme nella lista risultato e in modo ricorsivo chiamare nuovamente il resto delle liste locali e remoti. Guardate cosa sto lavorando con le definizioni tipo:

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

In termini semplici, io sto definendo due tipi. Uno è un tipo di record, che è davvero un oggetto senza alcuni della notazione tradizionale oggetto. L'altro è un tipo di unione discriminata, che è davvero il grafico di un oggetto/classe sotto mentite spoglie. Non voglio entrare nelle profondità della sintassi F # qui. Ci sono molte altre risorse per là fuori che, tra cui il mio libro, "Professional F # 2.0" (Wrox, 2010).

Basti dire, questi sono i tipi di input e output tipi, rispettivamente. Il motivo che ho scelto un'unione discriminata per il risultato, presto diventerà evidente. Date queste definizioni di due tipo, è abbastanza facile definire lo scheletro esterno di quello che voglio questa funzione come:

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

Ricordate, nel gergo di F #, che i descrittori di tipo vengono dopo il nome. Quindi questo è dichiarare una funzione che prende due transazioni elenca e restituisce un elenco di voci di registro. Come scritto, stub per restituire un elenco vuoto ("[]"). Questo è un bene, perché ora posso stub fuori alcune funzioni per testare — stile Test-Driven Development (TDD) — in una pianura vaniglia normale F # applicazione console.

Mi può e deve scrivere questi in un framework di unit test per ora, ma io posso realizzare essenzialmente la stessa cosa utilizzando Assert e funzioni localmente nidificate all'interno della conduttura. Gli altri possono preferiscono lavorare con F # REPL, in Visual Studio o dalla riga di comando, come illustrato Figura 1.

Figura 1 creare un algoritmo di Console con F # REPL

[<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

Dato che ho un'impalcatura di test di base sul posto, potrai attaccare la soluzione ricorsiva, come si può vedere Figura 2.

Figura 2 utilizzo F # criteri di ricerca per una soluzione ricorsiva

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

Come avrete notato, questo rende piuttosto pesante uso di corrispondenza di modello F #. Questo è concettualmente simile al blocco interruttore C# (allo stesso modo che un gattino è concettualmente simile a una tigre dai denti a sciabola). Innanzitutto, definire una funzione ricorsiva locale (rec) che è fondamentalmente la stessa firma della funzione esterna. C'è un parametro aggiuntivo per trasportare i corrispondenti risultati finora.

All'interno di questo, il primo blocco di partita esamina entrambe le liste locali e remote. La prima clausola della partita ([], _) dice che se l'elenco locale è vuoto, non mi interessa che cosa è la lista remota (la sottolineatura è un carattere jolly) perché ho finito. Quindi basta restituire i risultati ottenuti finora. Lo stesso vale per la seconda partita clausola (_, []).

La carne di tutta la faccenda viene nella clausola ultima partita. Questo estrae la testa della lista locale e si lega per il valore loc, il resto dell'elenco in locTail, fa lo stesso per il telecomando in rem e remTail e corrisponde quindi nuovamente. Questa volta, estrarre i campi importo da ciascuno dei due elementi sbucciati fuori fuori le liste e li legano in variabili locali locAmt e remAmt.

Per ognuno di questi match clausole, I'll ricorsivamente richiamare conciliare­interna. La differenza fondamentale è quello che faccio con la lista di outputSoFar prima recurse. Se il locAmt e remAmt sono gli stessi, è una partita, così io anteporre una nuova voce nella lista outputSoFar prima recursing. In ogni altro caso, basta ignorarli e recurse. Il risultato sarà un elenco di elementi di voce, e che è ciò che viene restituito al chiamante.

Estendere l'Idea

Supponiamo che non posso semplicemente ignorare tali elementi non corrispondenti. Ho bisogno di mettere un elemento nell'elenco risultante che dice che era una transazione locale senza eguali o una transazione remota senza eguali. Dell'algoritmo detiene ancora, basta aggiungere nuovi elementi nel registro discriminati Unione per tenere ognuno di tali possibilità e accodarlo nell'elenco prima di recursing, come mostrato Figura 3.

Figura 3 aggiungere nuovi elementi al registro

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

Ora i risultati saranno un elenco completo, con le voci di MissingLocal o MissingRemote per ogni transazione che non ha una corrispondente coppia. In realtà, questo non è assolutamente vero. Se le due liste sono mal adattate in lunghezza, come il mio caso test2 prima, gli elementi rimanenti non dato le voci "Mancante".

Prendendo F # come il "concettualizzare" lingua invece di c# e utilizzando i principi di programmazione funzionale, questo è diventato una soluzione abbastanza veloce. F # utilizza inferenza estesa, quindi in molti casi mentre scarnatura fuori il codice, non ho avuto determinare i tipi effettivi per i parametri e tornare prima del tempo. Le funzioni ricorsive in F # spesso bisogno un'annotazione del tipo per definire il tipo restituito. È scappato senza esso qui perché potrebbe dedurre dal ritorno tipo dato sull'esterno, che racchiude la funzione.

In alcuni casi, potrei solo questo compilato in un assembly e consegnarlo agli sviluppatori c#. Per un sacco di negozi, però, che non sta per volare. Così la prossima volta potrai convertire questo in C#. Il boss non saprà mai che questo codice effettivamente iniziato la vita come codice F #.

Codificazione felice!

Ted Neward è CTO di iTrellis, una società di consulenza servizi. Ha scritto più di 100 articoli e autore e coautore di una dozzina di libri, tra cui "Professional F # 2.0" (Wrox, 2010). Egli è un MVP di c# e parla a conferenze in tutto il mondo. Egli consulta e mentors regolarmente — contattarlo al ted@tedneward.com o ted@itrellis.com se siete interessati ad avere lui venire a lavorare con il vostro team, e leggere il suo blog a blogs.tedneward.com.

Grazie al seguente Microsoft esperto tecnico per la revisione di questo articolo: Lincoln Atkinson