C'è una Haskell equivalente di classi astratte di programmazione orientata agli oggetti, utilizzando i tipi di dati algebrici o polimorfismo?

StackOverflow https://stackoverflow.com/questions/4029455

Domanda

In Haskell, è possibile scrivere una funzione con una firma che può accettare due differenti (anche se simile) tipi di dati, e operano in modo diverso a seconda del tipo viene passato?

Un esempio potrebbe fare la mia domanda più chiara. Se ho una funzione denominata myFunction, e due tipi di nome MyTypeA e MyTypeB, posso definire myFunction in modo che può accettare solo i dati di tipo MyTypeA o MyTypeB come primo parametro?

type MyTypeA = (Int, Int, Char, Char)
type MyTypeB = ([Int], [Char])

myFunction :: MyTypeA_or_MyTypeB -> Char
myFunction constrainedToTypeA = something
myFunction constrainedToTypeB = somethingElse

In un linguaggio OOP, si potrebbe scrivere quello che sto cercando di realizzare in questo modo:

public abstract class ConstrainedType {
}

public class MyTypeA extends ConstrainedType {
    ...various members...
}

public class MyTypeB extends ConstrainedType {
    ...various members...
}

...

public Char myFunction(ConstrainedType a) {
    if (a TypeOf MyTypeA) {
        return doStuffA();
    }
    else if (a TypeOf MyTypeB) {
        return doStuffB();
    }
}

Ho letto sui tipi di dati algebrici e penso che ho bisogno di definire un Haskell tipo , ma non sono sicuro di come fare per definire in modo che esso può memorizzare un tipo o un'altra, e anche come lo uso nelle mie funzioni.

È stato utile?

Soluzione

Sì, lei ha ragione, siete alla ricerca di tipi di dati algebrici. C'è un tutorial su di loro a Imparare È un Haskell .

Per la cronaca, il concetto di una classe astratta da OOP ha in realtà tre diverse traduzioni in Haskell, e ADT sono solo uno. Ecco una rapida panoramica delle tecniche.

Tipi algebrica di dati

tipi di dati algebrici codificare il modello di una classe astratta cui sottoclassi sono noti, e dove le funzioni verificare quale esempio particolare l'oggetto è un membro di by-giù colata.

abstract class IntBox { }

class Empty : IntBox { }

class Full : IntBox {
    int inside;
    Full(int inside) { this.inside = inside; }
}

int Get(IntBox a) {
    if (a is Empty) { return 0; }
    if (a is Full)  { return ((Full)a).inside; }
    error("IntBox not of expected type");
}

si traduce in:

data IntBox = Empty | Full Int

get :: IntBox -> Int
get Empty = 0
get (Full x) = x

Record di funzioni

Questo stile non consente down-casting, quindi la funzione Get di cui sopra non sarebbe esprimibile in questo stile. Così qui è qualcosa di completamente diverso.

abstract class Animal { 
    abstract string CatchPhrase();
    virtual void Speak() { print(CatchPhrase()); }
}

class Cat : Animal {
    override string CatchPhrase() { return "Meow"; }
}

class Dog : Animal {
    override string CatchPhrase() { return "Woof"; }
    override void Speak() { print("Rowwrlrw"); }
}

La sua traduzione in Haskell non mappa i tipi in tipi. Animal è l'unico tipo, e Dog e Cat sono schiacciate via nelle loro funzioni di costruzione:

data Animal = Animal {
    catchPhrase :: String,
    speak       :: IO ()
}

protoAnimal :: Animal
protoAnimal = Animal {
    speak = putStrLn (catchPhrase protoAnimal)
}

cat :: Animal
cat = protoAnimal { catchPhrase = "Meow" }

dog :: Animal
dog = protoAnimal { catchPhrase = "Woof", speak = putStrLn "Rowwrlrw" }

Ci sono un paio di permutazioni diverse di questo concetto di base. L'invariante è che il tipo astratto è un tipo di record in cui i metodi sono i campi del record.

EDIT:. C'è una buona discussione nei commenti su alcune delle sottigliezze di questo approccio, tra cui un bug nel codice di cui sopra

Typeclasses

Questo è il mio preferito almeno la codifica di idee OO. E 'comodo per i programmatori OO perché utilizza parole familiari e mappe tipi di tipi. Ma il record di funzioni approccio di cui sopra tende ad essere più facile lavorare con quando le cose si complicano.

Io codificare di nuovo l'esempio degli animali:

class Animal a where
    catchPhrase :: a -> String
    speak       :: a -> IO ()

    speak a = putStrLn (catchPhrase a)

data Cat = Cat 
instance Animal Cat where
    catchPhrase Cat = "Meow"

data Dog = Dog
instance Animal Dog where
    catchPhrase Dog = "Woof"
    speak Dog = putStrLn "Rowwrlrw"

Questo sembra bello, non è vero? La difficoltà arriva quando ti rendi conto che, anche se sembra OO, non funziona davvero come OO. Si potrebbe desiderare di avere una lista di animali, ma il meglio che si può fare in questo momento è Animal a => [a], una lista di animali omogenei, ad esempio. un elenco dei soli gatti o cani solo. Allora avete bisogno di fare questo tipo di involucro:

{-# LANGUAGE ExistentialQuantification #-}

data AnyAnimal = forall a. Animal a => AnyAnimal a
instance Animal AnyAnimal where
    catchPhrase (AnyAnimal a) = catchPhrase a
    speak (AnyAnimal a) = speak a

E [AnyAnimal] poi è quello che volete per la vostra lista di animali. Tuttavia, si scopre che espone AnyAnimal esattamente le stesse informazioni su se stesso come il record Animal nel secondo esempio, abbiamo appena andato su di esso in modo indiretto. Così perché non ritengo typeclasses di essere un ottimo codifica di OO.

E così conclude edizione di questa settimana di Way Too Much Information!

Altri suggerimenti

Sembra che si potrebbe desiderare di leggere su typeclasses .

Si consideri questo esempio utilizzando TypeClasses .

Si definisce un C ++ - come MVC "classe astratta" sulla base di tre tipi (nota MultiParamTypeClasses): tState tAction tReaction al fine di definire una funzione tState -> tAction -> (tState, tReaction) chiave (quando viene applicata un'azione allo stato, si ottiene un nuovo Stato e una reazione.

Il typeclass ha tre funzioni "c ++ astratto", e alcuni più definiti sul "astratte" quelli. Le funzioni "astratti" saranno definiti quando e instance MVC è necessaria.

{-# LANGUAGE MultiParamTypeClasses, FunctionalDependencies, NoMonomorphismRestriction #-}


-- -------------------------------------------------------------------------------

class MVC tState tAction tReaction | tState -> tAction tReaction where
      changeState :: tState -> tAction -> tState       -- get a new state given the current state and an action ("abstract")
      whatReaction :: tState -> tReaction              -- get the reaction given a new state ("abstract")
      view :: (tState, tReaction) -> IO ()             -- show a state and reaction pair ("abstract")

      -- get a new state and a reaction given an state and an action (defined using previous functions)
      runModel :: tState -> tAction -> (tState, tReaction) 
      runModel s a = let
                                ns = (changeState s a) 
                                r = (whatReaction ns) 
                  in (ns, r)

      -- get a new state given the current state and an action, calling 'view' in the middle (defined using previous functions)
      run :: tState -> tAction -> IO tState
      run s a = do
                        let (s', r) = runModel s a
                        view (s', r)
                        return s'

      -- get a new state given the current state and a function 'getAction' that provides actions from "the user" (defined using previous functions)
      control :: tState -> IO (Maybe tAction) -> IO tState
      control s getAction = do
              ma <- getAction
              case ma of
                   Nothing -> return s
                   Just a -> do
                              ns <- run s a
                              control ns getAction


-- -------------------------------------------------------------------------------

-- concrete instance for MVC, where
-- tState=Int tAction=Char ('u' 'd') tReaction=Char ('z' 'p' 'n')
-- Define here the "abstract" functions
instance MVC Int Char Char where
         changeState i c 
                     | c == 'u' = i+1 -- up: add 1 to state
                     | c == 'd' = i-1 -- down: add -1 to state
                     | otherwise = i -- no change in state

         whatReaction i
                      | i == 0 = 'z' -- reaction is zero if state is 0
                      | i < 0 = 'n' -- reaction is negative if state < 0                     
                      | otherwise = 'p' -- reaction is positive if state > 0

         view (s, r) = do
                  putStrLn $ "view: state=" ++ (show s) ++ " reaction=" ++ (show r) ++ "\n"

--

-- define here the function "asking the user"
getAChar :: IO (Maybe Char) -- return (Just a char) or Nothing when 'x' (exit) is typed
getAChar = do
         putStrLn "?"
         str <- getLine
         putStrLn ""
         let c = str !! 0
         case c of
              'x' -> return Nothing
              _ -> return (Just c)


-- --------------------------------------------------------------------------------------------
-- --------------------------------------------------------------------------------------------

-- call 'control' giving the initial state and the "input from the user" function 
finalState = control 0 getAChar :: IO Int

-- 

main = do
     s <- finalState
     print s
Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top