Ruben
Ruben

Reputation: 524

Objects of multiple datatypes

I need to implement a chess game for a school assignment, and you have to make an interface that will work for other games on the same board. So, you have to implement chess pieces, but also pieces for other games.

I tried to do this:

data ChessPiece = King | Queen | Knight | Rook | Bishop | Pawn deriving (Enum, Eq, Show)
data Piece = ChessPiece | OtherGamePiece deriving (Enum, Eq, Show)
data ColoredPiece = White Piece | Black Piece
data Board = Board { boardData :: (Array Pos (Maybe ColoredPiece)) }

Then I try to load the begin f the chess game with:

beginBoard = Board (listArray (Pos 0 0, Pos 7 7) (pieces White ++ pawns White ++ space ++ pawns Black ++ pieces Black)) where
    pieces :: (Piece -> ColoredPiece) -> [Maybe ColoredPiece]
    pieces f = [Just (f Rook), Just (f Knight), Just (f Bishop), Just (f Queen), Just (f King), Just (f Bishop), Just (f Knight), Just (f Rook)]
    pawns :: (Piece -> ColoredPiece) -> [Maybe ColoredPiece]
    pawns f = (take 8 (repeat (Just (f Pawn))))
    space = take 32 (repeat Nothing)

And I get the error "Couldn't match expected type Piece' with actual typeChessPiece' In the first argument of f', namelyRook' In the first argument of Just', namely(f Rook)' In the expression: Just (f Rook)"

So, I've the feeling that the ChessPiece needs to be 'casted' to a (regular) Piece somehow. (I know, I am using terms from imperative programming, but I hope that I make myself clear here, I will be happy to make my question clearer if needed).

Is the construct that I'm trying to make possible? (sort of like a class structure from OO languages, but then applied to datatypes, where one datatype is a sub-datatype from the other, and an object can be two datatypes at the same time. For example a Rook is a ChessPiece and therefore a Piece) What am I doing wrong? Any suggestions on how to implement the structure I need?

Upvotes: 5

Views: 200

Answers (2)

Tikhon Jelvis
Tikhon Jelvis

Reputation: 68172

What you are after is normally referred to as sub-typing. Most OO languages achieve sub-typing using sub-classes.

Haskell, however, is decidedly not an OO language; in fact, it does not really have any sort of sub-typing at all. Happily, you can usually achieve much the same effect using "parametric polymorphism". Now, "parametric polymorphism" is a scary-sounding term! What does it mean?

In fact, it has a very simple meaning: you can write code that works for all (concrete) types. The Maybe type, which you already know how to use, is a great example here. The type is defined as follows:

data Maybe a = Just a | Nothing

note how it is written as Maybe a rather than just Maybe; the a is a type variable. This means that, when you go to use Maybe, you can use it with any type. You can have a Maybe Int, a Maybe Bool, a Maybe [Int] and even a Maybe (Maybe (Maybe (Maybe Double))).

You can use this approach to define your board. For basic board functions, you do not care about what "piece" is actually on the board--there are some actions that make sense for any piece. On the other hand, if you do care about the type of the piece, you will be caring about what the type is exactly, because the rules for each game are going to be different.

This means that you can define your board with some type variable for pieces. Right now, your board representation looks like this:

data Board = Board {boardData :: Array Pos (Maybe ColoredPiece)}

since you want to generalize the board to any sort of piece, you need to add a type variable instead of specifying ColoredPiece:

data Board p = Board {boardData :: Array Pos p}

now you've defined a Board type for any piece type you could possibly imagine!

So, to use this board representation for chess pieces, you need to pass the type of the piece to your new Board type. This will look something like this:

type ChessBoard = Board ColoredPiece

(For reference, type just creates a synonym--now writing ChessBoard is completely equivalent to writing Board ColoredPiece.)

So now, whenever you have a chess board, use your new ChessBoard type.

Additionally, you can write some useful functions that work on any board. For example, let's imagine all you want to do is get a list of the pieces. The type of this function would then be:

listPieces :: Board p -> [p]

You can write a whole bunch of other similar functions that don't care about the actual piece by using type variables like p in your function types. This function will now work for any board you give it, including a Board ColoredPiece, otherwise know as ChessBoard.

In summary: you want to write your Board representation polymorphically. This lets you achieve the same effect as you wanted to try with sub-typing.

Upvotes: 10

jtobin
jtobin

Reputation: 3273

Tikhon's solution is the way to go. FYI though, note the difference between a type constructor and a data constructor. Right here, for example:

data ChessPiece = King | Queen | Knight | Rook | Bishop | Pawn deriving (Enum, Eq, Show)
data Piece = ChessPiece | OtherGamePiece deriving (Enum, Eq, Show)

This won't work because you're defining a type constructor called ChessPiece in the first line and a data constructor called ChessPiece in the other, and these aren't the same thing. The type constructor says something like: "a ChessPiece type can be a King, or a Queen, or a..." while the data constructor just creates generic data (that also happens to be called ChessPiece).

What you can do is redefine the first data constructor for the Piece type; some generic data called ChessPiece that carries some information about the type ChessPiece under the hood. The following typechecks:

data ChessPiece   = King | Queen | Knight | Rook | Bishop | Pawn deriving (Enum, Eq, Show)
data Piece        = ChessPiece ChessPiece | OtherGamePiece  -- note the change 
data ColoredPiece = White Piece | Black Piece

and you could alter your functions like so:

pieces :: (Piece -> ColoredPiece) -> [Maybe ColoredPiece]
pieces f = [Just (f (ChessPiece Rook)), Just (f (ChessPiece Knight)), Just (f (ChessPiece Bishop)), Just (f (ChessPiece Queen)), Just (f (ChessPiece King)), Just (f (ChessPiece Bishop)), Just (f (ChessPiece Knight)), Just (f (ChessPiece Rook))]

To make the difference between type and data constructors more obvious, here's a limited version that that uses different names for each:

data ChessRoyalty = King | Queen
data Piece        = ChessPiece ChessRoyalty | OtherGamePiece
data ColoredPiece = White Piece | Black Piece

Upvotes: 2

Related Questions