2015-08-14 2 views
11

Non riesco a capire perché il codice seguente genera un errore. Se il secondo campo di Foo viene modificato in tipo Int, il codice viene eseguito senza errori.Tipo di sistema in Haskell

Prelude> data Foo = Foo {foo_a :: Int, foo_b :: Float} 
Prelude> let x = Foo 2 3.4 
Prelude> unwords $ map (\fn -> (show . fn) x) [foo_a, foo_b] 

<interactive>:4:46: 
    Couldn't match type `Float' with `Int' 
    Expected type: Foo -> Int 
     Actual type: Foo -> Float 
    In the expression: foo_b 
    In the second argument of `map', namely `[foo_a, foo_b]' 
    In the second argument of `($)', namely 
     `map (\ fn -> (show . fn) x) [foo_a, foo_b]' 

Perché show in grado di accettare gli argomenti di vario tipo? Il seguente, ovviamente, il lavoro:

Prelude> show $ foo_a x 
"2" 
Prelude> show $ foo_b x 
"3.4" 

Inoltre, dato che questo non funziona, qual è il modo consigliato di applicare show ai vari campi di un tipo di dati?

Grazie.

+0

Purtroppo non c'è modo semplice per fare quello che vuoi, perché diversi campi avranno tipi diversi e quindi non può essere gestito utilizzando gli elenchi e 'map'. Potrebbe esserci qualche soluzione compatta usando Template Haskell o qualche estensione più avanzata ... – Bakuriu

risposta

8

Il problema è che gli elenchi in Haskell sono omogenei (tutti gli elementi hanno lo stesso tipo). E in [foo_a, foo_b] si sta tentando di creare un elenco con due tipi diversi: Foo -> Int e Foo -> Float.

Una soluzione è quella di spostare il show all'interno della lista e creare un elenco di Foo -> String:

Prelude> data Foo = Foo {foo_a :: Int, foo_b :: Float} 
Prelude> let x = Foo 2 3.4 
Prelude> unwords $ map (\fn -> fn x) [show . foo_a, show . foo_b] 
"2 3.4" 

((\fn -> fn x) può essere scritta come ($ x))


Un'altra possibilità è quella di creare un tipo di dati per unificare i diversi tipi che si desidera inserire in un elenco, imitando uno heterogeneous collection. Nel tuo caso, potrebbe essere qualcosa di simile:

{-# LANGUAGE ExistentialQuantification #-} 

data Showable b = forall a . Show a => MkShowable (b -> a) 

pack :: Show a => (b -> a) -> Showable b 
pack = MkShowable 

unpack :: Showable b -> b -> String 
unpack (MkShowable f) = show . f 

Poi, si può fare:

*Main> let x = Foo 2 3.4 
*Main> unwords $ map (\fn -> unpack fn x) [pack foo_a, pack foo_b] 
"2 3.4" 

[Update]

stavo giocando in giro con il Data.Dynamic e sembra più promettente del tipo esistenziale che ho creato sopra.

Cominciamo con:

{-# LANGUAGE DeriveDataTypeable #-} 

import Control.Applicative 
import Data.Dynamic 
import Data.Maybe 

data Foo = Foo {foo_a :: Int, foo_b :: Float} 
    deriving Typeable 

get_a :: Dynamic -> Maybe (Foo -> Int) 
get_a = fromDynamic 

get_b :: Dynamic -> Maybe (Foo -> Float) 
get_b = fromDynamic 

getAsString :: Dynamic -> (Foo -> String) 
getAsString dyn = fromJust $ (show .) <$> get_a dyn 
          <|> (show .) <$> get_b dyn 
          <|> error "Type mismatch" 

In questo caso, possiamo fare:

*Main> let x = Foo 2 3.4 
*Main> unwords $ map (\fn -> getAsString fn x) [toDyn foo_a, toDyn foo_b] 
"2 3.4" 

Sembra che abbiamo dovuto scrivere più codice per ottenere lo stesso risultato, ma in realtà ci dà molta più flessibilità. Mentre il tipo esistenziale era focalizzato solo nel mostrare i campi come String, qui non siamo limitati a quello. Cosa succede se ora vogliamo vedere tutti i campi come Int (nel caso di Float vogliamo arrotondare il valore)?

getAsInt :: Dynamic -> (Foo -> Int) 
getAsInt dyn = fromJust $    get_a dyn 
         <|> (round .) <$> get_b dyn 
         <|> error "Type mismatch" 

Ora possiamo fare:

*Main> let x = Foo 2 3.4 
*Main> map (\fn -> getAsInt fn x) [toDyn foo_a, toDyn foo_b] 
[2,3] 
8

Questa è una limitazione del sistema di tipo Hindley-Milner. Solo le definizioni in let (e in modo equivalente, in where e al livello superiore) possono essere polimorfiche. In particolare, gli argomenti per le funzioni non possono essere polimorfici. fn deve avere il tipo Foo -> Int o Foo -> Float - non c'è il tipo Foo -> (Int or Float) o Foo -> Showable.

Se è necessario, è possibile definire un tipo visualizzabile, che è noto come existential type, ma è necessario fornire al tipografo qualche aiuto per utilizzarlo e nella maggior parte del codice l'idea non viene utilizzata perché il suo inconveniente supera la sua utilità, e generalmente non abbiamo problemi a esprimere ciò che vogliamo senza di esso.

+10

Principalmente corretto, ma ha una spiegazione più semplice: '[foo_a, foo_b]' è un 'Elenco a', quindi tutti gli elementi devono avere lo stesso tipo. Ma 'foo_a :: Foo-> Int' non può essere unificato con' foo_b :: Foo-> Float'. Fondamentalmente la stessa cosa con '[" String ", 1, True]'. – deniss