Is there a Haskell equivalent of OOP's abstract classes, using algebraic data types or polymorphism?

后端 未结 3 1648
说谎
说谎 2021-01-30 17:22

In Haskell, is it possible to write a function with a signature that can accept two different (although similar) data types, and operate differently depending on what type is pa

相关标签:
3条回答
  • 2021-01-30 17:43

    Consider this example using TypeClasses.

    We define a c++-like "abstract class" MVC based on three types (note MultiParamTypeClasses): tState tAction tReaction in order to define a key function tState -> tAction -> (tState, tReaction) (when an action is applied to the state, you get a new state and a reaction.

    The typeclass has three "c++ abstract" functions, and some more defined on the "abstract" ones. The "abstract" functions will be defined when and instance MVC is needed.

    {-# 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
    
    0 讨论(0)
  • 2021-01-30 17:51

    Yes, you are correct, you are looking for algebraic data types. There is a great tutorial on them at Learn You a Haskell.

    For the record, the concept of an abstract class from OOP actually has three different translations into Haskell, and ADTs are just one. Here is a quick overview of the techniques.

    Algebraic Data Types

    Algebraic data types encode the pattern of an abstract class whose subclasses are known, and where functions check which particular instance the object is a member of by down-casting.

    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");
    }
    

    Translates into:

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

    Record of functions

    This style does not allow down-casting, so the Get function above would not be expressible in this style. So here is something completely different.

    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"); }
    }
    

    Its translation in Haskell doesn't map types into types. Animal is the only type, and Dog and Cat are squashed away into their constructor functions:

    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" }
    

    There are a few different permutations of this basic concept. The invariant is that the abstract type is a record type where the methods are the fields of the record.

    EDIT: There is a good discussion in the comments on some of the subtleties of this approach, including a bug in the above code.

    Typeclasses

    This is my least favorite encoding of OO ideas. It is comfortable to OO programmers because it uses familiar words and maps types to types. But the record of functions approach above tends to be easier to work with when things get complicated.

    I'll encode the Animal example again:

    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"
    

    This looks nice, doesn't it? The difficulty comes when you realize that even though it looks like OO, it doesn't really work like OO. You might want to have a list of Animals, but the best you can do right now is Animal a => [a], a list of homogeneous animals, eg. a list of only Cats or only Dogs. Then you need to make this wrapper type:

    {-# LANGUAGE ExistentialQuantification #-}
    
    data AnyAnimal = forall a. Animal a => AnyAnimal a
    instance Animal AnyAnimal where
        catchPhrase (AnyAnimal a) = catchPhrase a
        speak (AnyAnimal a) = speak a
    

    And then [AnyAnimal] is what you want for your list of animals. However, it turns out that AnyAnimal exposes exactly the same information about itself as the Animal record in the second example, we've just gone about it in a roundabout way. Thus why I don't consider typeclasses to be a very good encoding of OO.

    And thus concludes this week's edition of Way Too Much Information!

    0 讨论(0)
  • 2021-01-30 18:02

    It sounds like you might want to read up on typeclasses.

    0 讨论(0)
提交回复
热议问题