2015-07-27 6 views
15

Il Prelude mostra esempi di take e drop con argomenti negativi:Perché `take` e` drop` sono definiti per argomenti negativi?

take (-1) [1,2] == [] 
drop (-1) [1,2] == [1,2] 

Perché questi definita come sono, per esempio quando x !! (-1) fa la cosa "più sicura" e si blocca? Sembra un modo hacker e molto non-Haskell per rendere totali queste funzioni, anche quando l'argomento non ha senso. C'è una filosofia di design più grande dietro a questo che non vedo? Questo comportamento è garantito dallo standard o è proprio questo il modo in cui GHC ha deciso di implementarlo?

+8

Ci sono cose ragionevoli e spesso utili da fare quando gli argomenti vanno oltre i limiti: "restituisce l'intero elenco originale" o "restituisce l'elenco vuoto". Non puoi assolutamente definire '(!! (-1))', non c'è alcun valore di riserva da restituire. –

+8

Un fatto divertente: la semantica cambiata tra i rapporti Haskell 98 e 2010. L'implementazione [esempio] (https://www.haskell.org/onlinereport/list.html) in 98 ha usato 'error' se la lista non era vuota e' n' era negativa, mentre la variante 2010 mostrava valori negativi ha lo stesso effetto di "prendi 0". – Zeta

+1

@Zeta: è affascinante, ottima scoperta! – Lynn

risposta

13

Ci sarebbe principalmente una buona ragione per rendere parziale take: potrebbe garantire che l'elenco dei risultati, se ce n'è uno, ha sempre il numero richiesto di elementi.

Ora, take viola già presente nella direzione opposta: quando si tenta di prendere più elementi che ci sono nella lista, è semplicemente prende tanti quanti sono, vale a dire meno di richieste. Forse non è la cosa più elegante da fare, ma in pratica tende a funzionare abbastanza bene.

L'invariante principale per take si coniuga con drop:

take n xs ++ drop n xs ≡ xs 

e che vale anche se n è negativo.

Una buona ragione non controlla la lunghezza della lista è che rende le funzioni eseguono bene su liste infinite pigri: per esempio,

take hugeNum [1..] ++ 0 : drop hugeNum [1..] 

immediatamente darà 1 come primo elemento risultato. Questo non sarebbe possibile se take e drop dovessero prima verificare se ci sono abbastanza elementi nell'input.

+0

Accettare questa risposta, perché 'prendere' già violando questo nella direzione opposta in un modo * utile * è un argomento abbastanza buono: non riesco a capire perché dovresti fare affidamento su come prendere (-1)' si comporta, ma scommetto che renderlo coerente nel modo in cui descrivi faceva parte dell'idea. – Lynn

6

Penso che sia una questione di scelta del design qui.

La definizione corrente assicura che la proprietà

take x list ++ drop x list == list 

vale per qualsiasi x, comprese quelle negative così come quelli più grandi di length list.

Posso tuttavia vedere il valore in una variante di take/drop quali errori: a volte un arresto anomalo viene preferito a un risultato errato.

+0

Ovviamente, un'implementazione parziale manterrebbe comunque il '(++). (accetta &&& drop) ≡ proprietà id'. – leftaroundabout

+0

@leftaroundabout L'LHS non si arresta? (A proposito, la tua equazione non scrive check) – chi

+1

che era pseudocodice. Quello che intendevo era, se 'drop n xs' e' take n xs' restituivano entrambi 'Nothing' quando' n ∉ [0 .. length xs-1] ', allora avremmo sempre' liftA2 (++) (take n xs) (drop n xs) ≡ Solo xs'. – leftaroundabout

4

x !! (-1) fa la cosa "più sicuro" e crash

d'arresto non è sicuro. Rendere una funzione non totale distrugge la tua capacità a causa del comportamento di una funzione in base al suo tipo.

Supponiamo che take e drop abbiano avuto un comportamento "in caso di arresto anomalo".Prendere in considerazione il loro tipo:

take, drop :: Int -> [a] -> [a] 

Una cosa di questo tipo sicuramente non vi dico che questa funzione potrebbe bloccarsi! È utile ragionare sul codice come se stessimo usando un linguaggio totale, anche se non lo siamo - un'idea chiamata fast and loose reasoning - ma per essere in grado di farlo, devi evitare di usare (e scrivere) funzioni non totali come il più possibile

Cosa fare, quindi, sulle operazioni che potrebbero non riuscire o non avere alcun risultato? I tipi sono la risposta! Un veramente sicuro variante (!!) avrebbe un tipo che modella il caso fallimento, come:

safeIndex :: [a] -> Int -> Maybe a 

Questo è preferibile al tipo di (!!),

(!!) :: [a] -> Int -> a 

Che, per la semplice osservazione, può non hanno abitanti (totali) - non puoi "inventare" un a se la lista è vuota!

Infine, torniamo a take e drop. Sebbene il loro tipo non dica completamente quale sia il comportamento, insieme ai loro nomi (e idealmente ad alcune proprietà QuickCheck) otteniamo una buona idea. Come altri soccorritori hanno sottolineato, questo comportamento è appropriato in molti casi. Se hai veramente hai la necessità di rifiutare gli input di lunghezza negativa, non devi scegliere tra non-totalità (arresto anomalo) o possibilità di comportamento sorprendente (lunghezza negativa accettata) - modellare i risultati possibili in modo responsabile con i tipi.

Questo tipo chiarisce che c'è "nessun risultato" per alcuni input:

takePos, dropPos :: Int -> [a] -> Maybe [a] 

Meglio ancora, questo tipo utilizza numeri naturali; Le funzioni con questo tipo non possono nemmeno essere applicate a un numero negativo!

takeNat, dropNat :: Nat -> [a] -> [a] 
+2

Capisco il tuo ragionamento, e cerco davvero di evitare le funzioni totali per i motivi che hai menzionato, ma resto ancora sul fatto che preferirei che le cose vadano * ovviamente completamente sbagliate * quando faccio qualcosa di strano come 'take (- 1) [5] 'di averli silenziosamente restituire un valore" abbastanza vicino "a ciò che * potrei * volere. Vedo come è meglio in alcuni casi, ma ci sono altri casi in cui 'length (take n xs) == n' essendo una legge valida sarebbe davvero bello! – Lynn

+2

Confrontate questo con la situazione con 'tail': potreste definire perfettamente' tail [] = [] '(ad esempio' tail = drop 1') e farebbe forse quello che volete ... a volte. Ma c'è un [ottimo motivo per cui non lo è] (https://en.wikipedia.org/wiki/Principle_of_least_astonishment). (Naturalmente, i tipi dipendenti sono la soluzione "più bella" per decisioni come questa, ma questa è tutta un'altra storia ...) – Lynn

+1

Sono più stupito quando una funzione fa qualcosa che il tipo dice che non dovrebbe fare, come il crash :) Non devi scegliere tra la totalità e il minimo stupore: hai solo bisogno di funzioni con tipi utili, ad es 'betterTail :: [a] -> Forse [a]'. – frasertweedale

Problemi correlati