2016-02-08 20 views
8

Non sono sicuro di come rispondere a questa domanda. Diciamo che sto provando a passare i percorsi dei file tmp, e voglio catturare l'idea che ci sono diversi formati di tmpfile, e ogni funzione funziona solo su uno di essi. Questo funziona:In Haskell, come si restringono le funzioni a un solo costruttore di un tipo di dati?

data FileFormat 
    = Spreadsheet 
    | Picture 
    | Video 
    deriving Show 

data TmpFile = TmpFile FileFormat FilePath 
    deriving Show 

videoPath :: TmpFile -> FilePath 
videoPath (TmpFile Video p) = p 
videoPath _ = error "only works on videos!" 

Ma ci deve essere un modo migliore di scriverlo senza errori di runtime giusto? Ho pensato a due alternative, questo:

type TmpSpreadsheet = TmpFile Spreadsheet 
type TmpPicture  = TmpFile Picture 
type TmpVideo  = TmpFile Video 

videoPath :: TmpVideo -> FilePath 

o questo:

data TmpFile a = TmpFile a FilePath 
    deriving Show 

videoPath :: TmpFile Video -> FilePath 

Ma ovviamente non compilazione. Qual è il modo giusto per farlo? Alcune altre idee, nessuno particolarmente interessante:

  • Wrap TmpFile nel formato invece che il contrario, per cui i valori sono Video (TmpFile "test.avi") ecc
  • fare un sacco di tipi di dati separati VideoTmpFile, PictureTmpFile ecc
  • Fare un typeclass TmpFile
  • Utilizzare le funzioni parziali ovunque, ma aggiungere funzioni di guardia di astrarre il pattern matching

Ho anche preso in considerazione l'apprendimento dell'estensione -XDataKinds, ma sospetto che manchi qualcosa di molto più semplice che si può fare senza di esso.

EDIT: Sto imparando molto oggi! Ho provato entrambi gli approcci descritti di seguito (DataKinds e tipi di fantasmi, che hanno costruttori di valori fittizi che possono essere rimossi con un'altra estensione), ed entrambi funzionano! Poi ho provato ad andare un po 'oltre. Entrambi ti permettono di creare un tipo annidato TmpFile (ListOf a) oltre al normale TmpFile a, che è bello. Ma ho deciso di andare con un tipo di fantasma puro (costruttori di valore intatto), perché è possibile creare un modello di corrispondenza su di essi. Ad esempio, sono rimasto sorpreso che questo in realtà funziona:

data Spreadsheet = Spreadsheet deriving Show 
data Picture  = Picture  deriving Show 
data Video  = Video  deriving Show 
data ListOf a = ListOf a deriving Show 

data TmpFile a = TmpFile a FilePath 
    deriving Show 

videoPath :: TmpFile Video -> FilePath 
videoPath (TmpFile Video p) = p 

-- read a file that contains a list of filenames of type a, 
-- and return them as individual typed tmpfiles 
listFiles :: TmpFile (ListOf a) -> IO [TmpFile a] 
listFiles (TmpFile (ListOf fmt) path) = do 
    txt <- readFile path 
    let paths = map (TmpFile fmt) (lines txt) 
    return paths 

vidPath :: TmpFile Video 
vidPath = TmpFile Video "video1.txt" 

-- $ cat videos.txt 
-- video1.avi 
-- video2.avi 
vidsList :: TmpFile (ListOf Video) 
vidsList = TmpFile (ListOf Video) "videos.txt" 

main :: IO [FilePath] 
main = do 
    paths <- listFiles vidsList -- [TmpFile Video "video1.avi",TmpFile Video "video2.avi"] 
    return $ map videoPath paths -- ["video1.avi","video2.avi"] 

Per quanto posso dire, l'equivalente con DataKinds è molto simile, ma non può accedere fmt come un valore:

{-# LANGUAGE DataKinds, KindSignatures #-} 

data FileFormat 
    = Spreadsheet 
    | Picture 
    | Video 
    | ListOf FileFormat 
    deriving Show 

data TmpFile (a :: FileFormat) = TmpFile FilePath 
    deriving Show 

vidPath :: TmpFile Video 
vidPath = TmpFile "video.avi" 

vidsList :: TmpFile (ListOf Video) 
vidsList = TmpFile "videos.txt" 

videoPath :: TmpFile Video -> FilePath 
videoPath (TmpFile p) = p 

listFiles :: TmpFile (ListOf a) -> IO [TmpFile a] 
listFiles (TmpFile path) = do 
    txt <- readFile path 
    let paths = map TmpFile (lines txt) 
    return paths 

main :: IO [FilePath] 
main = do 
    paths <- listFiles vidsList 
    return $ map videoPath paths 

(Può sembrare una cosa strana da volere, ma il mio programma effettivo sarà un interprete per un linguaggio piccolo che compila le regole Shake con un file tmp corrispondente a ciascuna variabile, quindi saranno utili gli elenchi digitati di tmpfile)

Sembra destra? Mi piace l'idea di DataKinds meglio, quindi ci andrei invece se potessi esaminarli come valori, o se risultasse che non è mai necessario.

+0

dipende un po 'da quello che vuoi fare - ad esempio per quello che ho visto dalla tua domanda sceglierei in effetti semplici (separati) wrapper 'newtype' come' newtype VideoPath = VideoPath FilePath' – Carsten

+1

o prendi il * tipo phantom * approccio: 'data tempfile a = tempfile FilePath' con' dati Videoformat' (GHC ha bisogno di estensione 'EmptyDataDecls') e poi lasciare che' videofile = tempfile myPath :: tempfile Videoformat' ... – Carsten

risposta

5

Hai ragione: con -XDataKinds, l'approccio TmpFile Video -> FilePath funzionava. E infatti penso che questa possa essere una buona applicazione per quell'estensione.

{-# LANGUAGE DataKinds #-} 

data TmpFile (a :: FileFormat) = TmpFile FilePath 
    deriving Show 

videoPath :: TmpFile Video -> FilePath 

Il motivo è necessario questa estensione di scrivere TmpFile Video è che i costruttori di FileFormat sono ab initio valore a livello (quindi solo esistono in fase di esecuzione), mentre TmpFile è di tipo a livello/tempo di compilazione.

Ovviamente c'è un altro modo per generare entità di livello testo: definire i tipi!

data Spreadsheet = Spreadsheet 
data Picture = Picture 
data Video = Video 

data TmpFile a = TmpFile a FilePath 
    deriving Show 

videoPath :: TmpFile Video -> FilePath 

Tali tipi sono chiamati tipi di fantasma. Ma in realtà, sono un po 'un trucco per aggirare la precedente mancanza di valori corretti a livello di tipo, che DataKinds ci ha dato ora. Quindi, a meno che non sia necessaria la compatibilità con i vecchi compilatori, utilizzare DataKinds!

Un'alternativa potrebbe essere non applicare il tipo di file in fase di compilazione, ma semplicemente rendere esplicito che le funzioni sono parziali.

data TmpFile = TmpFile FileFormat FilePath 
    deriving Show 

videoPath :: TmpFile -> Maybe FilePath 
videoPath (TmpFile Video p) = p 
videoPath _ = Nothing 

In realtà, questo approccio potrebbe essere l'uno più razionale, a seconda di cosa hai intenzione di fare.

+2

"tipo Phantom" di solito si riferisce a un costruttore di tipi che è parametrizzato da una variabile di tipo che non usa. –

+0

@DerekElkins: Penso che si riferisca ai _parameters_ che non sono effettivamente usati in qualche tipo. – leftaroundabout

+1

Sono d'accordo, direi "variabili tipo fantasma" non solo "tipi fantasma", ma non è così che le persone sembrano usarlo. [Tipo Phantom] (https://wiki.haskell.org/Phantom_type) –

2

Prima di tutto, vorrei evitare di usare estensioni così esotiche come "DataKinds" a meno che non ne abbiate assolutamente bisogno. Il motivo è piuttosto pratico e generale: più concetti linguistici vengono utilizzati per risolvere il problema, più difficile è ragionare sul codice.

Inoltre, "DataKinds" non è un concetto facile da avvolgere. È un concetto di transizione che attraversa contemporaneamente due universi: i valori e i tipi. Personalmente lo trovo piuttosto controverso e lo applicherei solo quando non avrò altra scelta.

Nel tuo caso hai già trovato due modi di affrontare il problema più semplice, senza "DataKinds":

  • Wrap tmpfile nel formato invece che il contrario, per cui i valori sono Video (tmpfile "test.avi"), ecc

  • fare un sacco di tipi di dati separati VideoTmpFile, PictureTmpFile ecc

Mi piace particolarmente l'idea dei tipi di avvolgimento, perché è flessibile e componibile. Ecco come mi piacerebbe costruire su di esso:

newtype Video a = 
    Video a 
    deriving (Functor, Foldable, Traversable) 

newtype Picture a = 
    Picture a 
    deriving (Functor, Foldable, Traversable) 

videoPath :: Video FilePath -> FilePath 

si può notare due cose:

  1. Video e Picture sono concetti generali, che non siano vincolati ad appena i file temporanei, e che già implementare alcune interfacce standard. Ciò significa che possono essere riutilizzati per altri scopi.

  2. Esiste uno schema evidente nelle definizioni di Video e Picture.


Il modello che vedete in Video e Picture può essere chiamato "tipi raffinatezza" e si astrae da in the "refined" package tra gli altri. Quindi potresti essere interessato a questo.


Per quanto riguarda le altre opzioni:

  • Fare un tmpfile typeclass

  • Utilizzare le funzioni parziali ovunque, ma aggiungere funzioni di guardia di astrarre il pattern matching

Questo è un "No" definito per entrambi. Non allevare tipeclass, lasciarli essere per i concetti veramente generali, che hanno leggi e probabilmente una teoria (Categoria) dietro di loro. Il linguaggio ti offre molti altri modi per astrarre. Inoltre, non consentire alle funzioni parziali di eseguire il crawling verso le tue API: c'è un consenso nella comunità sul fatto che sia un antipattern.

+0

Buon punto, penso di evitare di usare DataKinds perché è uno più concetto di linguaggio da ricordare, e lo trovo confuso (ha aggiunto dei segni di spunta, e i tipi sono omessi quando si verifica la corrispondenza dei pattern anche se compaiono nelle firme dei tipi di funzione). Tuttavia, non sono stato in grado di ottenere la corrispondenza del modello che funziona con i tipi di perfezionamento, perché non è possibile ottenere il valore effettivo senza prima specificare il formato. Ad esempio, proverei ad abbinare su 'listFiles (ListOf (fmt (TmpFile p)))', che non funziona senza conoscere 'fmt'. – Jeff