开发者

How to define a class that allows uniform access to different records in Haskell?

I have two records that both have a field I want to extract for display. How do I arrange things so they can be manipulated with the same functions? Since they have different fields (in this case firstName and buildingName) that are their name fields, they each need some "adapter" code to map firstName to name. Here is what I have so far:

class Nameable a where
  name :: a -> String

data Human = Human {
  firstName :: String
}

data Building = Building {
  buildingName :: String
}

instance Nameable Human where
  name x = firstName x

instance Nameable Building where
  -- I think the x is redundant here, i.e the following should work:
  -- name = buildingName
  name x = buildingName x

main :: IO ()
main = do
  putStr $ show (map name items)
  where
    items :: (Nameable a) => [a]
    items = [ Human{firstName = "Don"}
            -- Ideally I want the next line in the array too, but that gives an 
            -- obvious type error at the moment.
            --, Building{buildingName = "Empire State"}
            ]

This does not compile:

TypeTest.hs:23:14:
    Couldn't match expected type `a' against inferred type `Human'
      `a' is a rigid type variable bound by
          the type signature for `items' at TypeTest.hs:22:23
    In the expression: Human {firstName = "Don"}
    In the expression: [Human {firstName = "Don"}]
    In the definition of `items': items = [Human {firstName = "Don"}]

I would have expected the instance Nameable Human section would make this work. Can someone explain what I am doing wrong, and for bonus points what "concept" I am trying to get working, since I'm having t开发者_如何转开发rouble knowing what to search for.

This question feels similar, but I couldn't figure out the connection with my problem.


Consider the type of items:

items :: (Nameable a) => [a] 

It's saying that for any Nameable type, items will give me a list of that type. It does not say that items is a list that may contain different Nameable types, as you might think. You want something like items :: [exists a. Nameable a => a], except that you'll need to introduce a wrapper type and use forall instead. (See: Existential type)

{-# LANGUAGE ExistentialQuantification #-} 

data SomeNameable = forall a. Nameable a => SomeNameable a 

[...]

items :: [SomeNameable]
items = [ SomeNameable $ Human {firstName = "Don"},
          SomeNameable $ Building {buildingName = "Empire State"} ]

The quantifier in the data constructor of SomeNameable basically allows it to forget everything about exactly which a is used, except that it is Nameable. Therefore, you will only be allowed to use functions from the Nameable class on the elements.

To make this nicer to use, you can make an instance for the wrapper:

instance Nameable (SomeNameable a) where
    name (SomeNameable x) = name x

Now you can use it like this:

Main> map name items
["Don", "Empire State"]


Everybody is reaching for either existential quantification or algebraic data types. But these are both overkill (well depending on your needs, ADTs might not be).

The first thing to note is that Haskell has no downcasting. That is, if you use the following existential:

data SomeNameable = forall a. Nameable a => SomeNameable a

then when you create an object

foo :: SomeNameable
foo = SomeNameable $ Human { firstName = "John" }

the information about which concrete type the object was made with (here Human) is forever lost. The only things we know are: it is some type a, and there is a Nameable a instance.

What is it possible to do with such a pair? Well, you can get the name of the a you have, and... that's it. That's all there is to it. In fact, there is an isomorphism. I will make a new data type so you can see how this isomorphism arises in cases when all your concrete objects have more structure than the class.

data ProtoNameable = ProtoNameable {
   -- one field for each typeclass method
    protoName :: String
}

instance Nameable ProtoNameable where
    name = protoName

toProto :: SomeNameable -> ProtoNameable
toProto (SomeNameable x) = ProtoNameable { protoName = name x }

fromProto :: ProtoNameable -> SomeNameable
fromProto = SomeNameable

As we can see, this fancy existential type SomeNameable has the same structure and information as ProtoNameable, which is isomorphic to String, so when you are using this lofty concept SomeNameable, you're really just saying String in a convoluted way. So why not just say String?

Your items definition has exactly the same information as this definition:

items = [ "Don", "Empire State" ]

I should add a few notes about this "protoization": it is only as straightforward as this when the typeclass you are existentially quantifying over has a certain structure: namely when it looks like an OO class.

class Foo a where
    method1 :: ... -> a -> ...
    method2 :: ... -> a -> ...
    ...

That is, each method only uses a once as an argument. If you have something like Num

class Num a where
    (+) :: a -> a -> a
    ...

which uses a in multiple argument positions, or as a result, then eliminating the existential is not as easy, but still possible. However my recommendation to do this changes from a frustration to a subtle context-dependent choice, because of the complexity and distant relationship of the two representations. However, every time I have seen existentials used in practice it is with the Foo kind of tyepclass, where it only adds needless complexity, so I quite emphatically consider it an antipattern. In most of these cases I recommend eliminating the entire class from your codebase and exclusively using the protoized type (after you give it a good name).

Also, if you do need to downcast, then existentials aren't your man. You can either use an algebraic data type, as others people have answered, or you can use Data.Dynamic (which is basically an existential over Typeable. But don't do that; a Haskell programmer resorting to Dynamic is ungentlemanlike. An ADT is the way to go, where you characterize all the possible types it could be in one place (which is necessary so that the functions that do the "downcasting" know that they handle all possible cases).


I like @hammar's answer, and you should also check out this article which provides another example.

But, you might want to think differently about your types. The boxing of Nameable into the SomeNameable data type usually makes me start thinking about whether a union type for the specific case is meaningful.

data Entity = H Human | B Building
instance Nameable Entity where ...

items = [H (Human "Don"), B (Building "Town Hall")]


I'm not sure why you want to use the same function for getting the name of a Human and the name of a Building.

If their names are used in fundamentally different ways, except maybe for simple things like printing them, then you probably want two different functions for that. The type system will automatically guide you to choose the right function to use in each situation.

But if having a name is something significant about the whole purpose of your program, and a Human and a Building are really pretty much the same thing in that respect as far as your program is concerned, then you would define their type together:

data NameableThing =
  Human { name :: String } |
  Building { name :: String }

That gives you a polymorphic function name that works for whatever particular flavor of NameableThing you happen to have, without needing to get into type classes.

Usually you would use a type class for a different kind of situation: if you have some kind of non-trivial operation that has the same purpose but a different implementation for several different types. Even then, it's often better to use some other approach instead, like passing a function as a parameter (a "higher order function", or "HOF").

Haskell type classes are a beautiful and powerful tool, but they are totally different than what is called a "class" in object-oriented languages, and they are used far less often.

And I certainly don't recommend complicating your program by using an advanced extension to Haskell like Existential Qualification just to fit into an object-oriented design pattern.


You can try to use Existentially Quanitified types and do it like this:

data T = forall a. Nameable a => MkT a
items = [MkT (Human "bla"), MkT (Building "bla")]


I've just had a look at the code that this question is abstracting from. For this, I would recommend merging the Task and RecurringTaskDefinition types:

data Task
    = Once
        { name :: String
        , scheduled :: Maybe Day
        , category :: TaskCategory
        }
    | Recurring
        { name :: String
        , nextOccurrence :: Day
        , frequency :: RecurFrequency
        }
type ProgramData = [Task] -- don't even need a new data type for this any more

Then, the name function works just fine on either type, and the functions you were complaining about like deleteTask and deleteRecurring don't even need to exist -- you can just use the standard delete function as usual.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜