2014-05-05 7 views
5

Sto cercando di capire un modo ragionevole per consentire agli utenti della mia biblioteca di fornirmi un sacco di funzioni per controllare il modo in cui si comporta. Mi piacerebbe fornire loro alcune impostazioni predefinite che possono combinare e ignorare come meglio credono. Il modo ovvio (per me) è solo una registrazione di funzioni come Foo {a, b, c, d, e} e renderlo un monoide e quindi fornire alcuni valori predefiniti che possono mappend insieme. Ma le impostazioni predefinite che faccio non fornirebbero tutte le funzioni. Quindi potrei avere un record con {a, b} e uno con {c, d} e un altro con {b, c, e}. Questo ovviamente non è sicuro e un utente potrebbe fornirmi un record come {a, b, c, e} che sarebbe male. Voglio che l'utente sia in grado di mischiare e abbinare pezzi del genere, ma deve comunque finire con un record completo.Alternativa sicura alle registrazioni parziali?

Esiste un modo sicuro per fare qualcosa del genere? Se ho fatto tutte le funzioni del record in funzioni Maybe, almeno lo sto facendo in modo da poter verificare se il valore fornito manca di una funzione, ma in quel momento stanno ancora ricevendo quell'errore in fase di esecuzione invece del tempo di compilazione. Preferirei avere il "tutti i campi nel record devono essere forniti" invariabile applicato dal compilatore se posso.

+2

Se hai veramente bisogno di valori predefiniti parziali, non penso che ci sia una soluzione ordinata. Se si desidera la sicurezza in fase di compilazione, è necessario portare la distinzione a livello di tipo, attraverso entrambe (in ordine crescente di disordine) le funzioni di costruzione di Foo con diverse combinazioni di argomenti, un corrispondente tipo di somma 'PreFoo' con un costruttore per default parziale o più tipi pre-Foo separati che servono allo stesso scopo. – duplode

risposta

4

Stai cercando pacchetto data-default. Usando questo, puoi tranquillamente inizializzare i valori predefiniti per i tuoi tipi. Esempio:

import Data.Default 

data Foo = Foo { a :: Int, b :: Int } 

instance Default Foo where 
    def = Foo 3 3 

Ora utilizzando def è possibile utilizzare il valore di default in qualsiasi funzione è necessario:

dummyFun :: Foo -> Foo 
dummyFun x = def 

è anche possibile modificare il valore record come richiesto da voi:

dummyFun :: Foo -> Foo 
dummyFun x = def { b = 8 } 
+0

Non esiste un valore predefinito singolare che possa essere compilato per loro. Ecco perché hanno bisogno di fornire tutte le funzioni. – user3261399

+0

@ user3261399 Avrebbe senso fornire valori predefiniti arbitrari? Inoltre, potrei prendere il tuo esempio anche alla lettera, ma se puoi fornire, in diversi casi, '{a, b}', '{c, d}' e '{a, b, c, e}' come parziale i valori predefiniti quindi sembrano avere un valore plausibile predefinito per ogni campo. – duplode

0

È possibile utilizzare un tipo data con esattamente le combinazioni di opzioni desiderate. E non essere timido sacrificando la commutatività delle tue impostazioni monoid. Alla fine sarebbe come i programmi CLI con sottocomandi (come git, hg, dnf ecc.) La cui sintassi delle opzioni differisce.

0

La tua idea di Monoid è un buon inizio, ma Monoid non è abbastanza generale. Quello di cui hai veramente bisogno è un Category. Potresti crearne uno personalizzato per il lavoro, ma puoi anche evitare di pensare alle categorie semplicemente rinunciando all'idea di avere un singolo tipo fisso per rappresentare un record parziale e invece lasciare che i record parziali con campi diversi presenti abbiano tipi diversi. Questo significa che stai lavorando con la categoria di tipi e funzioni, a volte chiamate Hask, ma non devi pensarci. Avvertenza: uno dei fantastici pacchetti di record su Hackage probabilmente rende questo genere di cose molto più facile da fare, ma io non (ancora) capisco nessuno di loro abbastanza bene da usarli, per non parlare di raccomandarli.

Prima della caldaia-piastra

{-# LANGUAGE TypeFamilies #-} 
{-# LANGUAGE TypeOperators #-} 
{-# LANGUAGE GADTs #-} 
{-# LANGUAGE DataKinds #-} 
{-# LANGUAGE MultiParamTypeClasses #-} 
{-# LANGUAGE ScopedTypeVariables #-} 
{-# LANGUAGE FlexibleInstances #-} 

module PartialRec where 
import Data.Proxy 

Ora i tipi. PList rappresenta un record (parziale). Il primo elenco di argomenti rappresenta i tipi del campo del record, mentre il secondo elenco di argomenti indica quali campi sono presenti.

-- Skip is totally unnecessary, but makes the 
-- syntax of skips a bit less horrible. 
data Skip = Skip 
infixr 6 `PCons`, `PSkip` 
data PList :: [*] -> [Bool] -> * where 
    PNil :: PList '[] '[] 
    PCons :: a -> PList as bs -> PList (a ': as) ('True ': bs) 
    PSkip :: Skip -> PList as bs -> PList (a ': as) ('False ': bs) 

Usiamo una famiglia tipo di esprimere come i tipi combinano quando due record parziali sono combinati. In particolare, qualsiasi risultato presente nel record parziale sarà presente nel risultato.

type family Combine (as :: [Bool]) (bs :: [Bool]) :: [Bool] where 
    Combine '[] '[] = '[] 
    Combine ('True ': xs) (y ': ys) = 'True ': Combine xs ys 
    Combine ('False ': xs) (y ': ys) = y ': Combine xs ys 

La funzione combine combina due record parziali per formare uno nuovo con gli stessi tipi di campo ed eventuali campi presenti in entrambi atto parziale. Se un campo è presente in entrambi i record, viene scelto il primo.

combine :: PList as bs -> PList as cs -> PList as (Combine bs cs) 
combine PNil PNil = PNil 
combine (PCons x xs) (PSkip _ ys) = PCons x (combine xs ys) 
combine (PSkip _ xs) (PCons y ys) = PCons y (combine xs ys) 
combine (PSkip _ xs) (PSkip _ ys) = PSkip Skip (combine xs ys) 
combine (PCons x xs) (PCons _ ys) = PCons x (combine xs ys) 

La logica inadempiente è lasciato al buildRec. buildRec prende un record parziale con un set sufficiente di campi e produce valori per i campi facoltativi in ​​base ai campi obbligatori e ai campi facoltativi effettivamente presenti. buildRec viene effettivamente implementato utilizzando una classe di tipi con istanze scelte da una famiglia di tipi, per supportare più set di campi sufficienti.

-- Names for instances 
data BuilderTag = Builder1 | Builder2 

-- Given a list of types present, determines 
-- the correct Builder instance to use. 
type family ChooseBuilder (present :: [Bool]) :: BuilderTag where 
    ChooseBuilder '[ 'True, 'True, 'True, b3 ] = Builder2 
    ChooseBuilder '[ 'True, b1, 'True, b2 ] = Builder1 

class Builder (tag :: BuilderTag) (present :: [Bool]) where 
    buildRec' :: proxy tag -> PList '[Int, Char, Bool, Integer] present -> (Int, Char, Bool, Integer) 

buildRec :: forall tag present . (Builder tag present, tag ~ ChooseBuilder present) 
     => PList '[Int, Char, Bool, Integer] present -> (Int, Char, Bool, Integer) 
buildRec xs = buildRec' (Proxy :: Proxy tag) xs 

instance Builder 'Builder1 '[ 'True, b1, 'True, b2 ] where 
    buildRec' _ (i `PCons` Skip `PSkip` b `PCons` Skip `PSkip` PNil) = (i, toEnum (i + fromEnum b) , b, if i > 3 && b then 12 else 13) 
    buildRec' _ (i `PCons` Skip `PSkip` b `PCons` intg `PCons` PNil) = (i, toEnum (i + fromEnum b + fromIntegral intg), b, intg) 
    buildRec' _ (i `PCons` c `PCons` b `PCons` Skip `PSkip` PNil) = (i, c, b, fromIntegral i) 
    buildRec' _ (i `PCons` c `PCons` b `PCons` intg `PCons` PNil) = (i, c, b, intg) 

instance Builder 'Builder2 '[ 'True, 'True, 'True, b3 ] where 
    buildRec' _ (i `PCons` c `PCons` b `PCons` Skip `PSkip` PNil) = (i, c, b, fromIntegral i) 
    buildRec' _ (i `PCons` c `PCons` b `PCons` intg `PCons` PNil) = (i, c, b, intg) 

Ecco alcune funzioni per creare record parziali con un singolo campo ciascuno.

justInt :: Int -> PList '[Int, a, b, c] '[ 'True, 'False, 'False, 'False] 
justInt x = x `PCons` Skip `PSkip` Skip `PSkip` Skip `PSkip` PNil 

justChar :: Char -> PList '[a, Char, b, c] '[ 'False, 'True, 'False, 'False] 
justChar x = Skip `PSkip` x `PCons` Skip `PSkip` Skip `PSkip` PNil 

justBool :: Bool -> PList '[a, b, Bool, c] '[ 'False, 'False, 'True, 'False] 
justBool x = Skip `PSkip` Skip `PSkip` x `PCons` Skip `PSkip` PNil 

justInteger :: Integer -> PList '[a, b, c, Integer] '[ 'False, 'False, 'False, 'True] 
justInteger x = Skip `PSkip` Skip `PSkip` Skip `PSkip` x `PCons` PNil 

Ecco alcuni esempi di utilizzo. useChar terminerà utilizzando l'istanza Builder2, mentre noChar utilizzerà l'istanza Builder1.

useChar :: (Int, Char, Bool, Integer) 
useChar = buildRec $ justInt 12 `combine` justBool False `combine` justChar 'c' 

noChar :: (Int, Char, Bool, Integer) 
noChar = buildRec $ justInt 12 `combine` justBool False 
Problemi correlati