In this lab, we will meet the Cabal package manager, which helps us work on
projects with dependencies between multiple files. We will then program with the
codeworld-api
library, which provides us with types and functions for
drawing and transforming various shapes.
Before you start the lab, please make sure you have installed the
correct version of CodeWorld
as detailed in the installation instructions.
The constraints have been updated to ensure consistency throughout the course.
The content of this lab will be very useful for Assignment 1.
- Cabal build tool
- CodeWorld API
- References
Pre-lab Checklist
- You should be comfortable using command line arguments in the terminal, as in the Week 1 lab.
- You should be comfortable using Git Integration with VSCodium, as in the Week 2 lab.
- You should be able to read type signatures of functions in Haskell.
- You should be comfortable with writing function definitions, as in the Week 3 lab.
Learning Outcomes
- Read external documentation and understand how to use functions based on the type signature and description.
- Use the cabal package manager to maintain modules with internal dependencies.
- Use the codeworld-api library to draw shapes based on requirements.
- Gain introductory understanding in using lists.
Getting Started
-
Go to the project’s dashboard under the Project tab and click on the Fork button. The Gitlab url for this lab is https://gitlab.cecs.anu.edu.au/comp1100/2024s2/2024s2studentfiles/comp1100-2024s2-lab04
-
You will be asked where to fork the repository. Click on the user or group to where you’d like to add the forked project. You should see your own name here. Once you have successfully forked a project, you will be redirected to a new webpage where you will notice your name before
> project-name
. This means you are now the owner of this repository. The url in your browser should reflect the change: https://gitlab.cecs.anu.edu.au/uXXXXXXX/2024s2/2024s2studentfiles/comp1100-2024s2-lab04 -
Finally, clone your forked repository to your computer according to the instructions in the Week 2 Lab.
Gitlab CI
You may have noticed a new box on your repo on the Gitlab web view, looking something like this:
To help you ensure your submitted code is compiling, we’ve added a Continuous Integration (CI) script to this lab, and it will also be present in your assignments.
If your code is compiling successfully, the CI banner in the web interface will show a green tick, otherwise it will display a red x.
Cabal build tool
For this lab, we are using cabal: a build tool for Haskell that avoids the need to build all of the pieces of your project separately. The comp1100-lab04.cabal file lists all the requirements for the project. You do not need to understand how this file is written. In your own time, if you are interested feel free to explore the file with the documentation here.
There are several useful commands for cabal:
Cabal commands are executed in Terminal using Bash (not in ghci).
Cabal Command | Command Description |
---|---|
cabal v2-build |
Compiles all of the necessary Haskell files needed for the project. |
cabal v2-run |
Runs the executables, compiling files as necessary. In this case, it will run Main.hs by default as listed in the .cabal file. |
cabal v2-repl |
REPL stands for Read-Evaluate-Print Loop. This will launch the GHCi interpreter and load the main program. |
cabal v2-clean |
Ensures a fresh start for building, and prevents potential issues caused by lingering files from previous build operations. |
If you are having issues with cabal, run cabal v2-clean
and then run rerun the required command.
You will execute these cabal commands in the top-level directory of
your project, the directory containing the .cabal file (i.e., the directory
you are in when you clone/launch the VSCodium Terminal tool for your project).
You can check the presence of .cabal file by using the bash command ls
we learnt in week 1.
Modules
This project has a framework that divides the functionality into three primary
files in the src folder: Model.hs
, View.hs
, and
Controller.hs
. This partitioning of functionality is called the
Model-View-Controller (MVC) software
architecture. It is a common pattern used in implementing graphical user
interfaces (GUI).
This lab does not use Model or Controller since all the functionalities can be captured with the View module (as we will only be drawing pictures directly to the screen, and not reading any input from the keyboard or mouse). The information in this section is intended only to help guide you through Assignment 1 Part B, which does use this architecture.
The reason that the program is broken up like this is to increase readability and maintainability (so that you can find and fix problems more easily) by grouping related functions together. Moreover, there may be some functions in one module that other modules do not need. With modularisation, you can control what is exposed to other modules and what you want to use when importing.
Model
The Model defines the abstract problem domain, independently of the user
interface. The Model.hs
has been left empty in this lab since we are only
using types that are already defined in the codeworld-api
.
View
The View defines how we render the model for presentation to the user, in
graphical form. In this lab, we will be using codeworld-api
functions for
drawing shapes. All the functions in this lab will be implemented in this module
View.hs
.
Controller
The Controller implements the GUI-based mouse and keyboard input functionality of the application, allowing users to control the application.
CodeWorld API
This week we will be using the CodeWorld API (Application Programming Interface) to draw graphical pictures in a browser window. The API helps you easily draw pictures to the screen by writing Haskell code, using shapes, colours and transformations.
Let us begin by understanding the given lines of code.
Double click on the file Lab04/src/View.hs
. You should see code as
follows:
module View where
import CodeWorld
myPicture :: Picture
myPicture = blank
This line imports the functions defined by the CodeWorld API:
import CodeWorld
The next few lines declare a variable with name myPicture
of type
Picture
(“::
” is pronounced “has type”), and defines its value to be
blank
:
myPicture :: Picture
myPicture = blank
You can think of myPicture
as a function that takes no input, and returns
something of type Picture
. The idea of a function taking no input might seem
strange, but just as we can have functions that take one input,
In this lab you will be modifying the function myPicture
a number of times to make
different Picture
s appear. Keep a copy of your old versions of myPicture
in your
file, commented out, so that your tutor can give you your participation mark for your code.
Documentation for CodeWorld can be found at https://hackage.haskell.org/package/codeworld-api-0.8.0/docs/CodeWorld.html.
In this lab, we will focus on reading the documentation and using it to write functions. So, at any time during this lab (or later), feel free to refer to the documentation if you are not sure of how a function works. We suggest you open the documentation on a separate tab in your browser for ease of use.
The Picture
type and the value blank
were imported from the
CodeWorld API library. They are not part of the standard Haskell Prelude that
defines the standard types such as Int
, Double
etc. So the
import
statement is necessary before you can use any of these functions.
Double click on the file Lab04/src/Main.hs
. You should see code as
follows:
module Main where
import CodeWorld
import View
main :: IO ()
main = drawingOf myPicture
This imports the functions defined in the local View module:
import View
Before we explain the following lines
main :: IO ()
main = drawingOf myPicture
let’s have a look at what the functions in this module can achieve.
A blank screen
Make sure you are in the top-level directory (i.e. if you run ls
you should see the folder src
). In the terminal, execute cabal
v2-run
:
$ cabal v2-run
Resolving dependencies...
Configuring comp1100-lab04-1.0...
Preprocessing executable 'Main' for comp1100-lab04-1.0..
Building executable 'Main' for comp1100-lab04-1.0..
[1 of 4] Compiling Controller ( src/Controller.hs, dist/build/Main/Main-tmp/Controller.o )
[2 of 4] Compiling Model ( src/Model.hs, dist/build/Main/Main-tmp/Model.o )
[3 of 4] Compiling View ( src/View.hs, dist/build/Main/Main-tmp/View.o )
[4 of 4] Compiling Main ( src/Main.hs, dist/build/Main/Main-tmp/Main.o )
Linking dist/build/Main/Main ...
Running Main...
Open me on http://127.0.0.1:3000/
Click on http://127.0.0.1:3000/
to open your Web browser at that link. What
you see should not be too surprising, since myPicture
was defined to be
blank
.
Based on this, we can re-visit
main :: IO ()
main = drawingOf myPicture
So, main
is the name of a function that invokes a drawingOf
function. If we explore the CodeWorld documentation, we find the following:
drawingOf
:: Picture --The picture to show on the screen (in this case, myPicture).
-> IO ()
-- Draws a Picture.
What do you need to know about IO ()? I/O (Input/Output) allows you to write
programs that have input and output while they run. The type ()
, also
known as the unit type, contains one element only, and here means only that
the function is not intended to return anything useful, but causes
side-effects (in this case, drawing pictures to the screen).
The function drawingOf
takes a Picture
as an argument and performs
an output action but does not return anything useful.
Note: You are more than welcome to explore IO in your own time, but we will not invest time on this topic during this course. The only functions that you will be expected to write in this course will always be side-effect free.
Drawing a shape
Drawing a blank does not seem very useful. Let us draw something more interesting.
Before we get started, it may be useful to draw a grid.
Exercise 1 Coordinate Plane
myPicture
function so it returns a coordinate plane.You can use the CodeWorld documentation to find the appropriate function to draw such a plane. In this case, we will guide you towards the function required.
Once you have navigated to the webpage with the documentation, we can explore
the
Pictures
section since we are looking to return an output of the type Picture
. In
this section, we can see that the only function that meets our requirements
would be coordinatePlane
(scroll down the page a little to find it).
coordinatePlane :: HasCallStack => Picture
Feel free to ignore the HasCallStack
term, which you will also find in any
function of the type Picture
.
For anyone interested, =>
is called
the typeclass constraint, a concept we will explore later in this course.
Ignoring those details, we can read the type signature as follows:
coordinatePlane :: Picture
Now, we can modify the drawing by changing the function myPicture
:
myPicture = coordinatePlane
In order to test the function:
-
If you are still running the program displaying a blank screen, click on the Terminal and press
Ctrl+c
. This will kill the process that was running the previous I/O action. You can then reload the module usingcabal v2-run
.Linking dist/build/Main/Main ... Running Main... Open me on http://127.0.0.1:3000/ ^C $ cabal v2-run Preprocessing executable 'Main' for comp1100-lab04-0.1.0.. Building executable 'Main' for comp1100-lab04-0.1.0.. Running Main... Open me on http://127.0.0.1:3000/
-
If you had already closed the program earlier, you can follow the instructions provided earlier to draw the blank screen.
Submission required: Exercise 1 Coordinate Plane
.
Remember to add, commit and push your work.
What is a coordinate plane?
This plane combines two axes:
- The first axis line is horizontal, with positive numbers to the right and negative numbers to the left. You can use this axis to describe how far left or right to draw a picture.
- The second axis line is vertical, with positive numbers on top, and negative numbers on the bottom. You use this axis describe how far up or down to draw a picture.
You might also have heard of this grid by the name of a cartesian plane.
Exercise 2 Rectangle
Now that we have a grid, lets draw a shape. We will start with a rectangle. Draw a rectangle with a width of 2 units and height of 2 units.
You can use the appropriately named rectangle
function with the type
signature:
rectangle :: HasCallStack => Double -> Double -> Picture
Again, ignore the HasCallStack
term, so we can read the type signature as
follows:
rectangle :: Double -> Double -> Picture
myRectangle
that returns a rectangle of width and height of 2 units.The function should have the following type signature:
myRectangle :: Picture
Submission required: Exercise 2 Rectangle
Accessing cabal interpreter session
While cabal v2-run
configures, builds and runs the executable, it may be
useful to make the modules available in an interpreter session by using
cabal v2-repl
. Again, you need to be in the top-level directory to execute
the cabal command.
$ cabal v2-repl
Preprocessing executable 'Main' for comp1100-lab04-0.1.0..
GHCi, version 9.2.5: http://www.haskell.org/ghc/ :? for help
[1 of 4] Compiling Controller ( src/Controller.hs, interpreted )
[2 of 4] Compiling Model ( src/Model.hs, interpreted )
[3 of 4] Compiling View ( src/View.hs, interpreted )
[4 of 4] Compiling Main ( src/Main.hs, interpreted )
Ok, four modules loaded.
ghci>
In this session, you will be able to try out your existing code and also, test built-in functions in the loaded modules. It may also be useful to load modules outside the project and experiment with functions together with the already available modules.
You will find using cabal v2-repl very helpful for debugging code both here and in the assignment.
Try out the following.
ghci> drawingOf (thickRectangle 2 10 6)
Open me on http://127.0.0.1:3000/
Program is starting...
As usual for ghci, you can use :q
or Ctrl + D
to exit back to the Bash prompt.
Feel free to use cabal v2-repl
throughout the lab.
Exercise 3 Combining shapes
You can use the (&)
function to combine the pictures: the coordinate plane
and rectangle. which you can read as “and” or “in front of”.
myPicture
function so the output of the function looks like the following:Hint: You can use the (&)
function as follows. It can only combine
two pictures at a time:
combinedPicture = picture1 & picture2
In this case, picture1
will be in front of picture2
.
Test your function on your browser by following the steps in Exercise 1.
Submission required: Exercise 3 Rectangle on coordinate plane
Exercise 4 Make it solid
So you’ve just drawn an outline of a rectangle. What about a solid rectangle? Or a solid circle? Refer back to the CodeWorld API documentation if you are stuck.
myRectangle'
that returns a solid rectangle with the same dimensions as myRectangle
.Submission required: Exercise 4 Solid Rectangle
Transforming shapes
In CodeWorld, a transformation is a sort of function whose inputs are the original picture, along with a description of how to change the picture (colouring, moving, stretching etc.). The transformation produces a new picture that is like the original except for that change. There are a few different transformations such as colouring, translation, rotation, dilation, and scaling.
Exercise 5 Translation
The translated
function can change the position of a picture within the
coordinate plane. The function takes as arguments the original picture and two
distances to move the picture, and returns a new picture with the same content
as the original, but shifted either horizontally, or vertically, or both. Check
the documentation to find out the type signature of the function and learn its
usage.
myRectangle'
function to move the solid rectangle 3 units to the right and 2 units down.Submission required: Exercise 5 Translated Rectangle
Exercise 6 Colouring
The coloured
function modifies the colour of a picture. The function takes
as arguments the original picture and an input of type Colour
. The output
is a new picture with a colour depending on the input of the function. Check the
documentation to find out the type signature of the function and learn its
usage.
colouredRectangle
function that does not take any input but returns a blue myRectangle'
.Submission required: Exercise 6 Coloured Rectangle
Exercise 7 Create a logo
Let’s use these tools to design a cool logo. This is definitely not the logo of a big tech company!
Call the function colouredSquares
.
You will need to use the colours blue
, yellow
, green
and orange
.
Submission required: Exercise 7 Logo
Exercise 8 Rotation
The rotated
function takes two arguments: the original picture, and an
angle to rotate it. With this transformation, the result is a new picture that
has been rotated but retains all other features of the original picture.
Remember that rotated
function needs the input angle in
radians and will turn the picture anticlockwise.
colouredSquares
so the function now returns the shape but rotated 45 degrees.For more transformations, you can check out the scaled
and dilated
functions along with their usage on CodeWorld API documentation. Try to use
these transformations on the drawings we have already generated.
Submission required: Exercise 8 Rotated Logo
Exercise 9 Drawing Lambda
In this exercise, we will be using lists, which were introduced in the lectures. We will only be using them to draw simple shapes, so you are not required to know how to write functions that manipulate lists yet - we will be exploring this in more detail next week.
Custom shapes (lines and polygons) can be implemented using built-in functions in CodeWorld. We can use coordinates to define pictures composed of a sequence of line segments.
For example, try the following:
simpleLambda = coordinatePlane & polyline [(3,7), (7,1), (5, 4), (3,1)]
How does polyLine work?
If we look at the type definition for polyLine
(ignoring
HasCallStack
) in the Codeworld
documentation:
polyLine :: [Point] -> Picture
What is a Point
? If we click on Point
in the documentation, we
discover:
type Point = (Double, Double)
We have seen tuples before in Lab03. (Check them out there if you don’t remember.)
Point
is simply a pair of Double
s that represent a location in the
coordinate plane. This means that the function polyLine
takes a list of
these Point
s and draws line segments in between them.
myPicture
in the file so we can now visualize the drawing in the browser.lambda = coordinatePlane & polyline [(3,6), (7,0), (5, 3), (3,0)] & polyline [(2,6), (4,3), (2,0)]
solidLambda
that creates a drawing as below:Submission required: Exercise 9 Drawing Solid Lambda
.
You are required to submit your attempts at the exercises above in order to receive full participation marks for this lab. You have until the start of your next lab to do this. You should submit by committing and pushing all your changes to your Gitlab repository.
Extensions: Codeworld Events, and Animation
[Optional, not required for participation marks]
The next few exercises are quite difficult and require you to dive deep into CodeWorld documentation. The material of these exercises will not be examined but keen students are encouraged to attempt it.
This exercise is a challenge in reading and understanding code documentation. Our goal is to complete the following three exercises to end up with a rotating logo:
Exercise 10 Visualising The Logo
A visualisation function is a function that takes some model and maps it to something the user sees. We could say that a visualisation is a function that has this general type in Codeworld:
visualisation :: a -> Picture
visualisation model = ...
where a
is replaced by whichever type makes sense for this particular function.
Write a rotateLogo
function in View.hs
, as a visualisation for rotating our
logo picture.
- We need to change the
rotation
of the logo according to someDouble
input. - A large number should correspond to a large rotation, a small number to a small rotation.
- A negative number should correspond to rotation in the opposite direction.
rotateLogo :: Double -> Picture
rotateLogo theta = undefined
Exercise 11 Handling Events (Challenging)
An event is something that happens in our program that drives some new behaviour. Some examples of common program events are:
- The user clicks a button.
- The user presses a key.
- The current time ticks over to 5pm in the evening.
- A packet of data is received across the network.
The docs give us a lot of detail around events that Codeworld already recognises.
An event handler function is used to process events by taking the previous state as input, changing it to respond to an event, and returning the new state as output.
Write an eventHandler
function that only responds to the TimePassing
event,
while ignoring other events. This function should take in the x :: Double
state that changes as time passes, and update it depending on what event
occured. As you will see in the next exercise, this will interact with
rotatoLogo to ensure continuous rotation.
eventHandler :: Event -> Double -> Double
eventHandler e x = undefined
Exercise 12 Entry Point
Replace the entry point into our program in Main.hs
from a drawingOf
with an activityOf
.
Make sure you give your activity a suitable initial value for your world.
Then give it the eventHandler and rotateLogo Functions.
activityOf is a function that can take other functions as arguments. This is called a higher-order function, and we will examine in later in the course. For now, we have supplied the arguments for you below.
initialValue :: Double
initialValue = undefined
main :: IO ()
main = activityOf initialValue eventHandler rotateLogo
References
[1] Cabal User Guide, https://cabal.readthedocs.io/en/3.4/index.html
[2] CodeWorld API library, https://hackage.haskell.org/package/codeworld-api-0.8.0/docs/CodeWorld.html