Domanda

Ho una domanda sul fatto che un modo specifico di applicare il principio DRY sia considerato una buona pratica in Haskell. Presenterò un esempio e quindi chiederò se l'approccio che sto prendendo è considerato buono Stile Haskell. In poche parole, la domanda è questa: quando hai una formula lunga e poi ti ritrovi a dover ripetere alcuni piccoli sottoinsiemi di quella formula altrove, metti sempre quel sottoinsieme ripetuto della formula in una variabile in modo da puoi rimanere ASCIUTTO? Perché o perché no?

L'esempio: Immagina di prendere una stringa di cifre e di convertirla nel valore Int corrispondente. (A proposito, questo è un esercizio di "Real World Haskell").

Ecco una soluzione che funziona tranne che ignora i casi limite:

asInt_fold string = fst (foldr helper (0,0) string)
  where
    helper char (sum,place) = (newValue, newPlace)
      where 
        newValue = (10 ^ place) * (digitToInt char) + sum
        newPlace = place + 1

Utilizza foldr e l'accumulatore è una tupla del valore del posto successivo e della somma finora.

Finora tutto bene. Ora, quando sono andato a implementare i controlli del caso limite, ho scoperto che avevo bisogno di piccole porzioni del "nuovo valore" formula in luoghi diversi per verificare la presenza di errori. Ad esempio, sulla mia macchina, ci sarebbe un overflow Int se l'input fosse maggiore di (2 ^ 31 - 1), quindi il valore massimo che potrei gestire è 2.147.483.647. Pertanto, ho inserito 2 controlli:

  1. Se il valore del luogo 9 (il valore dei miliardi) e il valore della cifra sono > 2, c'è un errore.
  2. Se sum + (10 ^ place) * (digitToInt char) > maxInt, c'è un errore.

Questi 2 controlli mi hanno fatto ripetere parte della formula, quindi ho introdotto le seguenti nuove variabili:

  • digitValue = digitToInt char
  • newPlaceComponent = (10 ^ place) * digitValue

Il motivo per cui ho introdotto quelle variabili è semplicemente un'applicazione automatica del principio DRY: mi sono ritrovato a ripetere quelle parti della formula, quindi le ho definite una volta e una sola volta.

Tuttavia, mi chiedo se questo sia considerato un buon stile Haskell. Ci sono ovvi vantaggi, ma vedo anche degli svantaggi. Rende sicuramente il codice più lungo, mentre gran parte del codice Haskell che ho visto è piuttosto conciso.

Quindi, consideri questo buon stile Haskell e segui questa pratica o no? Perché / perché no?

E per quello che vale, ecco la mia soluzione finale che si occupa di una serie di casi limite e quindi ha un blocco piuttosto ampio. Puoi vedere quanto è diventato grande il blocco a causa della mia applicazione del principio DRY.

Grazie.

asInt_fold "" = error "You can't be giving me an empty string now"
asInt_fold "-" = error "I need a little more than just a dash"
asInt_fold string | isInfixOf "." string = error "I can't handle decimal points"
asInt_fold ('-':xs) = -1 * (asInt_fold xs) 
asInt_fold string = fst (foldr helper (0,0) string)
  where
    helper char (sum,place) | place == 9 && digitValue > 2 = throwMaxIntError
               | maxInt - sum < newPlaceComponent      = throwMaxIntError
                   | otherwise                             = (newValue, newPlace)
            where
              digitValue =  (digitToInt char)
              placeMultiplier = (10 ^ place)
              newPlaceComponent = placeMultiplier * digitValue
              newValue = newPlaceComponent + sum
              newPlace = place + 1
              maxInt = 2147483647
              throwMaxIntError = 
                        error "The value is larger than max, which is 2147483647"
È stato utile?

Soluzione

Come notato da bdonlan , il tuo algoritmo potrebbe essere più pulito --- è particolarmente utile che il linguaggio stesso rilevi un overflow. Per quanto riguarda il codice stesso e lo stile, penso che il principale compromesso sia che ogni nuovo nome impone un piccolo carico cognitivo sul lettore . Quando nominare un risultato intermedio diventa un appello di giudizio.

Personalmente non avrei scelto di nominare placeMultiplier , poiché ritengo che l'intento di place ^ 10 sia molto più chiaro. E vorrei cercare maxInt nel Preludio, poiché corri il rischio di essere terribilmente sbagliato se eseguito su hardware a 64 bit. Altrimenti, l'unica cosa che trovo discutibile nel tuo codice sono le parentesi ridondanti. Quindi quello che hai è uno stile accettabile.

(Le mie credenziali: a questo punto ho scritto nell'ordine da 10.000 a 20.000 righe di codice Haskell e ne ho letto forse due o tre volte. Ho anche dieci volte quella esperienza con la famiglia di lingue ML , che richiedono al programmatore di prendere decisioni simili.)

Altri suggerimenti

DRY è un principio altrettanto valido in Haskell come in qualsiasi altro luogo :) Molte delle ragioni alla base della terseness di cui parli in haskell è che molti modi di dire vengono portati in biblioteca e che spesso quegli esempi che guardi sono stati considerati con molta attenzione per renderli concisi :)

Ad esempio, ecco un modo alternativo per implementare il tuo algoritmo digit-to-string:

asInt_fold ('-':n) = negate (asInt_fold n)
asInt_fold "" = error "Need some actual digits!"
asInt_fold str = foldl' step 0 str
    where
        step _ x
            | x < '0' || x > '9'
            = error "Bad character somewhere!"
        step sum dig =
            case sum * 10 + digitToInt dig of
                n | n < 0 -> error "Overflow!"
                n -> n

Alcune cose da notare:

  1. Rileviamo l'overflow quando succede, non decidendo limiti arbitrari su quali cifre consentiamo. Ciò semplifica in modo significativo la logica di rilevamento dell'overflow e la fa funzionare su qualsiasi tipo intero da Int8 a Integer [fintanto che l'overflow risulta avvolgente, non si verifica o provoca un'affermazione da parte dell'operatore addizionale stesso]
  2. Usando una piega diversa, non abbiamo bisogno di due stati separati.
  3. Non ci ripetiamo, anche senza fare di tutto per sollevare le cose - cade naturalmente dal ribadire ciò che stiamo cercando di dire.

Ora, non è sempre possibile semplicemente riformulare l'algoritmo e far sparire la duplicazione, ma è sempre utile fare un passo indietro e riconsiderare come hai pensato al problema :)

Penso che il modo in cui lo hai fatto abbia senso.

Dovresti certamente suddividere sempre i calcoli ripetuti in valori definiti separatamente se evitare il calcolo ripetuto è importante, ma in questo caso non sembra necessario. Tuttavia, i valori scomposti hanno nomi facili da capire, quindi rendono più facile seguire il tuo codice. Non penso che il tuo codice sia un po 'più lungo di conseguenza è una cosa negativa.

A proposito, invece hardcoding il massimo Int, puoi usare (maxBound :: Int) che evita il rischio che tu commetta un errore o un'altra implementazione con un Int massimo diverso che rompa il tuo codice.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top