简体   繁体   中英

What is the equivalent pattern in haskell for replacing conditionals with polymorphism?

I've been exploring Haskell to get some functional programming experience. I'm hoping to find patterns that bring some insight so I can write better c++. For instance, I find the normal implementation of converting a switch statement to polymorphic code unsatisfying.

void HandleMessage(int type, Message m)
{
   switch (type)
   {
   case 1:
      HandleType1(m);
      break;
   case 2:
      HandleType2(m);
      break;
   default:
      Log("error, unhandled message type")
   }
}

becomes:

void HandleMessage(Type t, Message m)
{
   std::unique_ptr<HandlerInterface> handler = HandlerFactory.GetHandler(t);
   handler.handleMessage(m);
}

std::unique_ptr<HandlerInterface> HandlerFactory::GetHandler(Type t)
{
   switch (t)
   {
   case 1:
      return std::make_unique<HandlerType1>();
   case 2:
      return std::make_unique<HandlerType2>();
   default:
      return std::make_unique<DefaultHandler>();
   }
}

This just pushes the switch to the factory. It's probably better than the first solution, but it just feels like there should be something better.

I was wondering if a better pattern exists in Haskell. I know you can use guards:

handleMessage t msg
   | t == 1 = handleType1 msg
   | t == 2 = handleType2 msg

But this doesn't seem that much more elegant. You could switch the types as ints to a proper type and do pattern matching:

data Type = Type1 | Type2

createType t
   | t == 1 = Type1
   | t == 2 = Type2

handleMessage Type1 msg = handleType1 msg
handleMessage Type2 msg = handleType2 msg

Again, we can push the switch around, but I'm not really seeing something elegant. I'm hoping for several things:

  1. Cohesion: code is bundled together well.
  2. Adding a new handle requires little boilerplate. None would be preferred!
  3. Nice to have: Missing a handler for a particular type would be a compilation error.

Your Haskell solutulion is a near copy of the first C++ fragment. It only looks better because of Haskell's terser syntax, but it does exactly the same thing. Actually an exact C++ equivalent would be

enum Type { Type1, Type2 };
...
Type getType(int type) {
  switch (type) {
      case 1: return Type1;
      case 2: return Type2;
    ...
 ...
 switch (getType(type)) {
     case Type1: // etc etc

As you can see there isn't too much of wonderful and exciting functional programming going on. Indeed, functional programming is about programming with functions as first class values . There ain't any in your example.

Let's change it a bit.

....
handler Type1 = handleType1
handler Type2 = handleType2

So now handler is a primitive higher-order function. It gets a Type and returns another function. This still isn't much, but this translates to C++ this way:

void handleType1(Message);
void handleType2(Message);

auto getHandler(Type type) {
  switch(type) {
      case Type1: return handleType1;
      case Type2: return handleType2;
    ....

So instead of returning an object, you return a function. This is not a coimcidence. An object is just a bunch of functions (its methods). An object with one method is just a function!

Wait, what about data? Isn't an object made up of methods and data ? It is, but its data can be bound in a function (think lambda or std::bind). Normal C++ functions (function pointers) cannot capture context, but you just use lambdas and std::function.

So I guess this example doesn't bring too much insight to the table in the end. Instead of objects,use functions, that's all.

You can do pattern matching on Int:

createType 1 = Type1
createType 2 = Type2
createType _ = error("error, unhandled message type")

handleMessage n msg = handleType (createType n) msg

handleType Type1 msg = doSome...
handleType Type2 msg = doSome2...

But if you don't want to do that, you can directly do pattern matching on Types:

handleType Type1 msg = handle1 msg
handleType Type2 msg = handle2 msg

But even better would be using a type class:

class Handler a where
  handleMsg :: a -> String -> IO ()


data Type = Type1 | Type2

instance Handler Type where
  handleMsg Type1 = handle1
  handleMsg Type2 = handle2

handle1 msg = putStrLn $ "handle1 " ++ msg
handle2 msg = putStrLn $ "handle2 " ++ msg 

main = do
  handleMsg Type1 "error 1"
  handleMsg Type2 "error 2"

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM