meditans
meditans

Reputation: 586

How to correct my OOP tendencies when programming in Haskell

I have this recurrent problem when programming in Haskell. At some point, I try to simulate an OOP approach. Here I was writing some sort of AI for a flash game I found, and I'd like to describe the various pieces and the level as a list of pieces.

module Main where

type Dimension = (Int, Int)
type Position = (Int, Int)
data Orientation = OrienLeft | OrienRight

data Pipe = Vertical | Horizontal | UpLeft | UpRight | DownLeft | DownRight
data Tank = Tank Dimension Orientation
data Bowl = Bowl Dimension
data Cross = Cross
data Source = Source Dimension

-- desired
-- data Piece = Pipe | Tank | Bowl | Cross | Source

-- So that I can put them in a list, and define
-- data Level = [Piece]

I know I should abstract the functionalities and put Them in a list, but I often feel blocked in the process of writing code. What is the general mindset I should have in these situations?

Upvotes: 13

Views: 737

Answers (4)

comonad
comonad

Reputation: 5251

I had similar problems 10 years ago, when I learned haskell. Let me answer this in a more general way than your example above.


TL;DR:

Think in terms of OOP-like objects, that represent value-objects and function-objects, with heavy use of c++Templates/javaGenerics. Your program design should be based on data-flow through functions instead of references-to-mutable-memory that store intermediate object states. Those function-objects are composable at runtime.


Your statement "At some point, I try to simulate an OOP approach." describes your experienced way of designing programs. As you might have heard somewhere, functional programming is considered harder to learn for OOP programmers than for newbies, because "wrong concepts" need to be unlearned first. That is not completely true, if you know how to use that to your advantage:

The trick is to use your OOP experience or more likely your imagination on "objects" in the opposite direction: How would you design haskell functions and haskell values with objects? How would you restructure your program to design a sane data flow using only these new concepts? In functional programming, every "value" is an object with only final/const properties -- or with a getter that allows lazy initialization (and with that it allows to implement seemingly infinite single linked lists). "Functions" are such "values", too. If you think about your programm using these concepts, you will easily master this without OOP design patterns but with "objects" that are actually functions and values.

If OOP is about managing memory cells that store mutable states/values, then functions are about calculating the next state/value from the previous. At some point you will see that you only think about combining functions for a sane dataflow and not about managing memory-locations-that-store-values.

The next step would be to see instances of type-classes as some kind of globally instanciated dictionary-objects that are automagically passed as invisible parameters to functions. The OOP classes of these dictionary-objects would use c++Templates/javaGenerics as type parameters, and their methods relate to their params and/or return value instead of any "this" reference. As soon as you understand when and how to use type-classes, they will become properties/roles/flavours of your types instead of imaginary objects.

IMHO, type-classes are one of the best/sanest OOP-design-patterns, but they are only known to very very few programmers and are not easily reinvented without functional thinking. (Every experienced haskell programmer that I know personally has reinvented type-classes as design-pattern in c++Templates/objC/js...)

The general way to learn to think in haskell or any other new strange language is to ask the question: "What can I do with this language easily and how?" And not the question: "How can I write this specific design that actually fits to a completely different language that I'm already very experienced in?" (This sounds obvious, but we tend to forget that. Again and again.)


If your program really needs objects more in the OOP sense, then you might be looking for haskells records (if you are a beginner). If those objects represent databases or similar things, you might be looking for lens libraries; but lenses might be (or might not be) very advanced/irritating for beginners. Lenses are composable getterAndSetter "objects"(or values or function like things) that are used like "obj.lens1.lens2.modify()" in OOP -- which are in OOP stackable but neither composable nor objects themself.

Upvotes: 1

J. Abrahamson
J. Abrahamson

Reputation: 74364

You're on your way to some great code. Let me push it a few more steps toward a Haskell-like solution.

You've successfully modeled each Piece as an independent entity. This looks completely fine as is, but you want to be able to work with collections of pieces. The most immediate way to do this is to describe a type which can be any of the pieces desired.

data Piece = PipePiece   Pipe
           | TankPiece   Tank
           | BowlPiece   Bowl
           | CrossPiece  Cross
           | SourcePiece Source

which would let you write a list of pieces like

type Kit = [Piece]

but requires that when you consume your Kit that you pattern match on the different kinds of Pieces

instance Show Piece where
  show (PipePiece   Pipe)   = "Pipe"
  show (TankPiece   Tank)   = "Tank"
  show (BowlPiece   Bowl)   = "Bowl"
  show (CrossPiece  Cross)  = "Cross"
  show (SourcePiece Source) = "Source"

showKit :: Kit -> String 
showKit = concat . map show

There's also a strong argument for reducing the complexity of the Piece type by "flattening" out some redundant information

type Dimension   = (Int, Int)
type Position    = (Int, Int)
data Orientation = OrienLeft | OrienRight
data Direction   = Vertical | Horizontal | UpLeft | UpRight | DownLeft | DownRight

data Piece = Pipe Direction
           | Tank Dimension Orientation
           | Bowl Dimension
           | Cross
           | Source Dimension

which eliminates many redundant type constructors at the expense of no longer being able to reflect what kind of piece you have in the type of a function—no longer can we write

rotateBowl :: Bowl -> Bowl
rotateBowl (Bowl orientation) = Bowl (rotate orientation)

but instead

rotateBowl :: Piece -> Piece
rotateBowl (Bowl orientation) = Bowl (rotate orientation)
rotateBowl somethingElse      = somethingElse

which is pretty annoying.

Hopefully that highlights some of the tradeoffs between those two models. There's at least one "more exotic" solution which uses type classes and ExistentialQuantification to "forget" about everything besides an interface. This is worth exploring as it's pretty tempting to do but is considered to be a Haskell anti-pattern. I'll describe it first then talk about the better solution.

To use ExistentialQuantification we remove the sum type Piece and create a type class for pieces.

{-# LANGUAGE ExistentialQuantification #-}

class Piece p where
  melt :: p -> ScrapMetal

instance Piece Pipe
instance Piece Bowl
instance ...

data SomePiece = forall p . Piece p => SomePiece p

instance Piece SomePiece where
  melt (SomePiece p) = melt p

forgetPiece :: Piece p => p -> SomePiece
forgetPiece = SomePiece

type Kit = [SomePiece]

meltKit :: Kit -> SomePiece
meltKit = combineScraps . map melt

This is an antipattern because ExistentialQuantification leads to more complex type errors and the erasure of lots of interesting information. The usual argument goes that if you're going to erase all information besides the ability to melt the Piece, you ought to have just melted it to begin with.

myScrapMetal :: [ScrapMetal]
myScrapMetal = [melt Cross, melt Source Vertical]

And if your typeclass has multiple functions then perhaps your real functionality is stored in that class. For instance, let's say we can melt a piece and also sell it, perhaps the better abstraction would be the following

data Piece = { melt :: ScrapMetal
             , sell :: Int
             }

pipe :: Direction -> Piece
pipe _ = Piece someScrap 2.50

myKit :: [Piece]
myKit = [pipe UpLeft, pipe UpRight]

In all honesty, this is almost exactly what you're getting via the ExistentialQuantification method, but much more directly. When you erase the type information via forgetPiece you leave only the typeclass dictionary for class Piece---this is exactly a product of the functions in the typeclass which is what we're explicitly modeling with the data Piece type just described.


The one reason I can think of to use ExistentialQuantification is best exemplified by Haskell's Exception system—if you're interested, take a look at how it's implemented. The short of it being that Exception had to be designed such that anyone could add a new Exception in any code and have it be routable through the shared Control.Exception machinery while maintaining enough identity for the user to catch it as well. This required the Typeable machinery as well... but it's almost certainly overkill.


The takeaway should be that the model you use will depend a lot on how you end up consuming your data type. Initial encodings where you represent everything as an abstract ADT like the data Piece solution are nice in that they throw away little information... but can also be both unwieldy and slow. Final encodings like the melt/sell dictionary are often more efficient, but require deeper knowledge about what a Piece "means" and how it will be used.

Upvotes: 15

Guiraldelli
Guiraldelli

Reputation: 532

In my opinion, there is no problem in the way you are thinking: it is fairly abstract and that is good.

As suggested by Sassa NF, you can use type classes and that would be very elegant. But in your example, I would expand it in a "simpler way", using abstract data type, since it seems the "natural way" that your thinking.

In this sense, your example would be something like, for example:

data Piece = Vertical 
           | Horizontal 
           | UpLeft 
           | UpRight 
           | DownLeft 
           | DownRight
           | Cross
           | Bowl Dimension
           | Source Dimension
           | Tank Dimension Orientation

And repeating it: I do not see problems in your way of modeling the problem since it seems abstract enough to me.

Upvotes: 1

Sassa NF
Sassa NF

Reputation: 5406

You are thinking of polymorphism. There is place for that in Haskell, too, only it is done differently.

For example, it seems you want to process Pieces in a Level in a generic fashion. What is that processing? If you can define those functions, you will find that's like defining a Piece interface. In Haskell that will be a typeclass (defined as class Piece a with a list of functions the "implementations" should take care of).

Then you would need to define what those functions do for particular data types, for example instance Piece Pipe and add the definitions of those functions. Once you've done that for all data types, you can add them to the list of Pieces.

Upvotes: 0

Related Questions