3
votes

I'm playing around with a toy project in Haskell. I'm implementing some data structures I've built in other languages before to familiarize myself with how they'd be built in Haskell. This isn't my first functional language, I've built a couple of projects like a Scheme interpreter in OCaml but I think my OCaml experience is coloring how I'm approaching this problem. Its' not terribly important, but may be useful for context, to know that the data structure I'm implementing is a PR-Quadtree.

What I want to do is match and destructure a type inside a guard, a la OCaml's match statement.

data Waypoint = WayPoint {
    lat :: Float,
    lon :: Float,
    radius :: Float,
    speed :: Float,
    accel :: Float
} deriving (Show)

data Region = Region {
    x :: Float,
    y :: Float,
    width :: Float
} deriving (Show)

data PRQuadtree = WhiteNode Region
    | BlackNode Region Waypoint 
    | GreyNode { 
        topLeft :: PRQuadtree,
        topRight :: PRQuadtree,
        botLeft :: PRQuadtree,
        botRight :: PRQuadtree,
        region :: Region
    } deriving (Show)

getRegion node 
    | BlackNode(r, _) = r
    | WhiteNode(r) = r
    | GreyNode = region node 

The getRegion function is the one I am having problems with in particular. In case what I'm trying to do is unclear: I'd like to simple extract one element of the argument but that depends on which member of the algebraic data type the argument is. In OCaml I could do:

let getRegion node = match node with
    | BlackNode(r, _) = r
    | WhiteNode(r) = r
    | GreyNode = region(node)

(or something very similar, my OCaml is a bit rusty now).

In Haskell however, this doesn't appear to bind r in scope of the RHS of the guard branch. I've tried to look up Pattern Guards, as they sound similar to what I might want to do, but I can't really grok whats going on here. Really I just want to get a binding from the LHS of the = to the RHS of the equals (depending on which branch of the guard we've gone down).

Whats the idiomatic Haskell way to do what I'm trying to do here?

2

2 Answers

6
votes

It can be achieved as follows:

getRegion :: PRQuadtree -> Region
getRegion (BlackNode r _) = r
getRegion (WhiteNode r) = r
getRegion GreyNode{region=r} = r

or even as

getRegion :: PRQuadtree -> Region
getRegion x = case x of
  BlackNode r _ -> r
  WhiteNode r -> r
  GreyNode{} -> region x

In Haskell, prepending a type signature is very idiomatic.

Another option is extending the region field to the other cases as well:

data PRQuadtree = WhiteNode { region :: Region }
    | BlackNode { region :: Region , waypoint :: Waypoint }
    | GreyNode { 
        topLeft :: PRQuadtree,
        topRight :: PRQuadtree,
        botLeft :: PRQuadtree,
        botRight :: PRQuadtree,
        region :: Region
    } deriving (Show)

Now, region will work on all PRQuadtree values.

Haskell uses | as ML does when defining algebraic datatypes, to separate different constructors, but does not use it to separate case branches, which instead follow the syntax

case .. of { pat1 -> e1 ; pat2 -> e2 ; ... }

which can be replaced by indentation

case .. of
   pat1 -> e1
   pat2 -> e2
   ...

Also, note that partial field selectors are discouraged:

data A = A1 { foo :: Int } | A2

Above, foo A2 type checks but crashes. On the other hand, when a field is present in all the constructors, we do not face such risk.

3
votes

You can also write:

getRegion x 
    | BlackNode y <- x -> ....
    | Greynode{}  <- x -> ....

but it is quite unidiomatic in this simple case.

However, in more complex programs, this pattern matching in guards can be very useful. You use multiple equations or case to distinguish the general cases, like shown by @chi. But then, you can detect special cases, like in the following made up example:

getRegion x = case x of
    BlackNode{region} 
        | [(0,_)] <- filter (inRegion region) interestingPoints
            -> -- region encloses exactly 1 interesting point on x axis
               ....
        | otherwise = ....
        where
           interestingPoints = .....
           inRegion :: Region -> Point -> Bool
    GreyNode{} -> ....