Everything you need to know about Haskell
☙ to be an ❧
Amateur Sewage Engineeer
Robin KAY
How can we do this
in Haskell?
In this talk
I. Model with Types
II. Manipulate with Functions
III. Wrap with a View
Part I
Modelling with Types
Let's write some Haskell...
data Bearing
= North
| East
| South
| West
Bearing is a data type representing the cardinal directions
North
East
South
West
Tile Data Type
data Tile
= Start Bearing
Start is a constructor function which takes a Bearing as a paramter.
Start North
Start East
Start South
Start West
Other Tile Constructors
data Tile
= Start Bearing
| End Bearing
| Corner Bearing
| Straight Orientation
| Cross
data Orientation
= Horizontal
| Vertical
End North
End East
End South
End West
Corner North
Corner East
Corner South
Corner West
Straight Horizontal
Straight Vertical
Cross
Representing the Playfield
Map Point Tile
Map.fromList [(Point 1 3, Start North),
(Point 1 2, Straight Vertical),
(Point 1 1, Corner East),
(Point 2 1, Cross),
(Point 4 4, Corner North)]
The Point data type is simply defined as:
data Point = Point Int Int
Perfection?
☑ Type-system enforces legal values
☑ Maps well onto user input
☑ At most one Tile per Point
Not For All Seasons
⚙ Game logic needs to know when and where the Sewage flows
☑ Homogenous Representation
☑ Explicit Representation
Key Observeration
Sewage enters and exits each tile
data Plumb = Plumb Bearing Bearing
Corner North
Plumb North East
Plumb East North
The Start and End tiles
data Plumb = Plumb (Maybe Bearing) (Maybe Bearing)
Maybe is a polymorphic data type representing the presence or absence of a value
data Maybe a = Just a | Nothing
Plumb (Just North) (Just East)
Plumb (Just East) (Just North)
Plumb Nothing (Just North)
Plumb (Just North) Nothing
North
East
South
West
Nothing
North
—
East
—
South
—
West
—
Nothing
—
What about the cross tile?
decompose into a pair of straight pipes
→
+
[Plumb (Just South) (Just North), Plumb (Just East) (Just West)]
Trade Off
☆ Tile
is closer to the Problem: Safer and more Approachable
☆ Plumb
is closer to the Solution: Homogeneous and Explicit
Both have their Place
Part II
Manipulating with Functions
Back to Basics
Rotate a Bearing by 90 degrees clockwise
bend :: Bearing -> Bearing
bend North = East
bend East = South
bend South = West
bend West = North
with pattern matching
Bending with Combinators
Other rotations can be built out of the bend function
and the dot operator
-- 180 degrees clockwise or anti-clockwise
invert :: Bearing -> Bearing
invert = bend . bend
-- 270 degrees clockwise or 90 degrees anti-clockwise
revBend :: Bearing -> Bearing
revBend = bend . bend . bend
Higher Ordered Bending
Our bending operation can be lifted using fmap
bendF :: (Functor a) => a Bearing -> a Bearing
bendF = fmap bend
Maybe is a Functor
*Main> bendF (Just North)
Just East
*Main> bendF Nothing
Nothing
Lists are Functors
*Main> bendF [North, South]
[East, West]
*Main> bendF []
[]
Travelling By Bearing
with more pattern matching
nextPos :: Maybe Bearing -> Point -> Point
nextPos (Just North) (Point x y) = Point x (y-1)
nextPos (Just East) (Point x y) = Point (x+1) y
nextPos (Just South) (Point x y) = Point x (y+1)
nextPos (Just West) (Point x y) = Point (x-1) y
nextPos Nothing p = p
Convert one front-end Tile into back-end Plumb
plumbTile :: (Point -> Maybe Tile) -> PlumbState -> Maybe Plumb
☑ Tile Map Lookup Function
☑ Current Position and Flow Direction
data PlumbState = PlumbState Point (Maybe Bearing)
⬇
☆ Maybe a piece of Plumbing
data Plumb = Plumb Point (Maybe Bearing) (Maybe Bearing)
Straight tiles
plumbTile :: (Point -> Maybe Tile) -> PlumbState -> Maybe Plumb
plumbTile getTile (PlumbState curr flow) = case getTile curr of
Just (Straight Horizontal)
| flow == Just East || flow == Just West ->
Just $ Plumb curr (fmap invert flow) flow
Just (Straight Vertical)
| flow == Just North || flow == Just South ->
Just $ Plumb curr (fmap invert flow) flow
...
Guard conditions check that the sewage flow is legal for the tile
Corner tiles
plumbTile :: (Point -> Maybe Tile) -> PlumbState -> Maybe Plumb
plumbTile getTile (PlumbState curr flow) = case getTile curr of
...
Just (Corner theta)
| flow == Just (invert theta) ->
Just $ Plumb curr (Just theta) (Just $ bend theta)
| flow == Just (revBend theta) ->
Just $ Plumb curr (Just $ bend theta) (Just theta)
...
Cross tiles
plumbTile :: (Point -> Maybe Tile) -> PlumbState -> Maybe Plumb
plumbTile getTile (PlumbState curr flow) = case getTile curr of
...
Just Cross -> Just $ Plumb curr (fmap invert flow) flow
...
This rule doesn't have a Guard because a Cross tile can be entered from any Bearing
Start and End tiles
plumbTile :: (Point -> Maybe Tile) -> PlumbState -> Maybe Plumb
plumbTile getTile (PlumbState curr flow) = case getTile curr of
...
Just (Start theta)
| flow == Nothing ->
Just $ Plumb curr Nothing (Just theta)
Just (End theta)
| flow == Just (invert theta) ->
Just $ Plumb curr (Just theta) Nothing
...
The flow is Nothing at the start
...and also Nothing at the end
Fallback case
plumbTile :: (Point -> Maybe Tile) -> PlumbState -> Maybe Plumb
plumbTile getTile (PlumbState curr flow) = case getTile curr of
...
_ -> Nothing
If no Tile was present then no Plumb is produced
If the guard conditions aren't satisfied then no Plumb is produced for the Tile
Extending plumbTile
to plumb an entire pipe
unfoldr
generates lists by iteratively applying a function to state
unfoldr :: (b -> Maybe (a, b)) -> b -> [a]
unfoldr is a polymorphic function
a
The thing being generated
b
The state of the generator
unfoldr stops when the supplied function returns Nothing
Unfolding Applied to Plumbing
plumbPipe :: Map Point Tile -> Point -> [Plumb]
plumbPipe grid start =
unfoldr (fmap f . plumbTile (flip Map.lookup grid)) $
PlumbState start Nothing
where f plumb@(Plumb curr flow) =
(plumb, PlumbState (nextPos flow curr) flow)
☆ Initial state with no flow
☆ Iterate over trying to plumb each Tile
☆ Helper generates new PlumbState
[(Point 1 3, Start North)
,(Point 1 2, Straight Vertical)
,(Point 1 1, Corner East)
,(Point 2 1, Cross)]
⬇
plumbTile' $ PlumbState (Point 1 3) Nothing
[(Point 1 3, Start North)
,(Point 1 2, Straight Vertical)
,(Point 1 1, Corner East)
,(Point 2 1, Cross)]
⬇
plumbTile' $ PlumbState (Point 1 2) (Just North)
⬇
[Plumb (Point 1 3) Nothing (Just North)
[(Point 1 3, Start North)
,(Point 1 2, Straight Vertical)
,(Point 1 1, Corner East)
,(Point 2 1, Cross)]
⬇
plumbTile' $ PlumbState (Point 1 1) (Just North)
⬇
[Plumb (Point 1 3) Nothing (Just North)
,Plumb (Point 1 2) (Just South) (Just North)
[(Point 1 3, Start North)
,(Point 1 2, Straight Vertical)
,(Point 1 1, Corner East)
,(Point 2 1, Cross)]
⬇
plumbTile' $ PlumbState (Point 2 1) (Just East)
⬇
[Plumb (Point 1 3) Nothing (Just North)
,Plumb (Point 1 2) (Just South) (Just North)
,Plumb (Point 1 1) (Just South) (Just East)
[(Point 1 3, Start North)
,(Point 1 2, Straight Vertical)
,(Point 1 1, Corner East)
,(Point 2 1, Cross)]
⬇
plumbTile' $ PlumbState (Point 3 1) (Just East)
⬇
[Plumb (Point 1 3) Nothing (Just North)
,Plumb (Point 1 2) (Just South) (Just North)
,Plumb (Point 1 1) (Just South) (Just East)
,Plumb (Point 2 1) (Just West) (Just East)
]
What's missing?
Spare Parts
Spare Parts
Spare Parts
Two Step Solution
☑ Remove the tiles we've already plumbed from the grid
☑ Generate dummy Plumbs for the remaining tiles
foldl'
generates state by iteratively applying a function to list elements
foldl' :: (b -> a -> b) -> b -> [a] -> b
foldl' is a polymorphic function
a
The things being consumed
b
The state of the fold
Remove the already plumbed tiles from the grid
stripPlumbed :: [Plumb] -> Map Point Tile -> Map Point Tile
stripPlumbed ps grid =
foldl' (\grid' (Plumb p theta _) ->
Map.update (f theta) p grid') grid ps
where f (Just North) Cross = Just $ Straight Horizontal
f (Just East) Cross = Just $ Straight Vertical
f (Just South) Cross = Just $ Straight Horizontal
f (Just West) Cross = Just $ Straight Vertical
f _ _ = Nothing
foldl' calls Map.update to remove each tile in turn
...except the Cross tiles, which need special handling
Only the Spare Parts remain
Map.foldWithKey is a specialised fold
spareParts :: Map Point Tile -> [Plumb]
spareParts grid =
Map.foldWithKey sparePart [] grid
Pick an arbitrary direction of flow for each tile
sparePart :: Point -> Tile -> [Plumb] -> [Plumb]
sparePart p (Corner theta) xs =
Plumb (-1) p (Just theta) (Just $ bend theta) : xs
sparePart p (Straight Horizontal) xs =
Plumb (-1) p (Just East) (Just West) : xs
...
Plumbed Tiles with Spare Parts
Sneak Preview
Video in WebM format.
Part III
Wrapping with a View
QML
Qt Quick Toolkit
Haskell's Love-to-Hate GUI Toolkit
What is QML?
😃 QML is a Domain Specific Language for creating User Interfaces
😒 It's not a Haskell DSL though
What is QML?
☆ Describes a hierarchy of visual Items
☆ Items have properties
☆ Properties can be data-bound to your Haskell model
QML Example
import QtQuick 2.0
Rectangle {
width: 300; height: 200;
color: 'blue';
Text {
anchors.centerIn: parent;
color: 'white'; font.pixelSize: 30;
text: 'Hello World';
}
}
QML Example in Action
Domain Types relevant to the View
data Grid = Grid Int Int (Map Point Tile)
Can be exposed as Objects to QML
instance DefaultClass Grid where
classMembers = [
defPropertyConst "width" (\(Grid w _ _) ->
return w),
defPropertyConst "height" (\(Grid _ h _) ->
return h),
defMethod "place" (\grid x y tile ->
return $ placeTile (Point x y) tile grid :: IO Grid),
defMethod "plumb" (\grid ->
return $ plumbGrid grid :: IO [Plumb])]
Properties
Methods
Repeater data-binds to the plumbing produced by our Haskell program
Repeater {
model: gridModel.plumb();
Plumb {
tileSize: c.tileSize;
x: modelData.x*c.tileSize;
y: modelData.y*c.tileSize;
entry: modelData.entry;
exit: modelData.exit;
colour: modelData.colour;
volume: modelData.idx < 0 ? 0 :
Math.min(Math.max(c.totalVolume-modelData.idx,0),1.1);
}
}
The animation is driven from QML
Pros and Cons of HsQML
☹ You have to write QML
☹ No Haskell EDSL for QML
☺ Haskell Code is Pure
☺ Expedient Solution
😈 Let QML Deal With It
Fin
http://www.gekkou.co.uk/software/hsqml