--- ---
For this project, you will implement the guts of a (fancy) calculator. I have provided you with a simple read-eval-print interface (in CalcREPL.hs) that lets the user type in expressions to be evaluated. You will ultimately provide a function of type String -> String
which accepts the user’s input and produces a response. Of course, your String -> String
function should be decomposed into multiple phases, just like all of the language implementations we have been considering (such as parsing, pretty-printing, interpreting, and so on). This project intentionally gives you much less guidance than the first project in terms of what specific data types and functions you should write, and how to decompose your solution. However, you can of course use the modules we have done as templates to help guide you.
Download the provided zip file, which should contain several files including CalcREPL.hs
, Calc.lhs
, Parsing2.hs
, and a few configuration files such as calc.cabal
and stack.yaml
.
Extract the contents of the zip file.
Using repl.it
repl.it
to complete this project. Simply upload all the provided files to a new repl.it
project.Working locally
You can also install Haskell locally on your own computer.
The easiest way to do this is to first install the ghcup
tool.
Then use ghcup
at a terminal prompt to install GHC as well as the cabal
build tool:
ghcup install ghc
ghcup install cabal
You can now use ghci
just as you would in repl.it
. The only difference is that you may have to pass some -package
flags to tell it what packages to use, for example
ghci -package parsec Calc.lhs
Whether you are working in a repl.it
shell or in a terminal window on your own computer, the process for compiling and running your calculator is the same:
Compile and run your calculator using the command cabal run
.
You should get a calculator prompt where you can enter expressions (though it will not do anything yet).
Simply exit the calculator and rerun cabal run
every time you want to test changes you have made to Calc.lhs
.
Your calculator must support the following features:
Double
values)+
, -
, *
, /
, and ^
as well as prefix negation:help
messages which explain/illustrate the features of the calculator.For example, a sample interaction with your calculator might look like this:
> 2+3
5
> (((3*5) - 9) + -8.3)
-2.3000000000000007
> 2 ^ 2 ^ 2 ^ 2
65536.0
> (3+3)*3
18.0
> 3+
(line 1, column 3):
unexpected end of input
expecting end of "+", number, or "("
Your calculator must never crash with a runtime error or pattern-match failure.
Get started by editing the starter code below and adding to it as appropriate!
reserved
token parser to parse things like function names, names of constants or units, etc.naturalOrFloat
token parser to parse literal values that can either be an integer or a floating-point value. Note it does not handle negatives; that should be taken care of automatically by your prefix negation operator.fromIntegral
to convert from Integer
to Double
.parse
function to run your parser. If it returns an error wrapped in a Left
constructor, you can simply call show
on the resulting error to turn it into a String
appropriate for displaying to the calculator user.parseSome
function can be used as before for experimenting with parsers in GHCi.Double
values in Haskell is done with the (**)
operator. (The (^)
operator is only for integers.){-# LANGUAGE GADTs #-}
module Calc where
import Parsing2
import qualified Data.Map as M
Edit this description and replace it with your own! It gets printed when the calculator interface first starts up.
description :: String
= unlines
description "Welcome to my calculator."
[ "This boring message is being shown because"
, "I have not bothered to update it."
, "Features this calculator supports: none."
, "Type an expression, :help, or :quit."
, ]
Edit this help message and replace it with your own! It gets printed when the user types :help
. Adding some well-chosen examples could be a good way to concisely show off the different features of your calculator.
helpMsg :: String
= unlines
helpMsg "You can use integers or floating point values,"
[ "negation, or standard arithmetic operators + - * / ^ ."
, ]
This is the main function that is called by CalcREPL
to evaluate user input.
calc :: String -> String
= "Implement me!" calc input
To complete this project to Level 2, in addition to the requirements for Level 1:
Re-display a nicely formatted version of the user’s input as confirmation of each computation. For example, a sample interaction with your calculator might now look like this:
> 2+3
2.0 + 3.0
= 5.0
> (((3*5) - 9) + -8.3)
3.0 * 5.0 - 9.0 + -8.3
= -2.3000000000000007
> 2 ^ 2 ^ 2 ^ 2
2.0 ^ 2.0 ^ 2.0 ^ 2.0
= 65536.0
> (3+3)*3
(3.0 + 3.0) * 3.0
= 18.0
Ensure that your code uses good Haskell style.
Make sure your code is simplified as much as possible, for example, without redundant pattern-matching.
Turn on {-# OPTIONS_GHC -Wall #-}
and make sure your code generates no warnings.
Write informative, grammatically correct comments explaining your code, its operation, and any choices you made along with the reasons for those choices.
To complete this project to Level 3, in addition to the requirements for Level 2, you must complete at least two extensions. You may pick from the following list of suggested extensions (ordered roughly from easier to harder), or propose your own.
Add support for the constants π and e, along with at least five functions such as sine, cosine, tangent, log, floor, ceiling, round, square root, or absolute value. For example, a sample interaction might look like this:
> sin(pi/6)
sin(π / 6.0)
= 0.49999999999999994
> cos(tan(log(abs(-2))))
cos(tan(log(abs(-2.0))))
= 0.6744026976311414
> ((1 + sqrt(5))/2)^2 - 1
((1.0 + sqrt(5.0)) / 2.0) ^ 2.0 - 1.0
= 1.618033988749895
Support for complex numbers. For example, the user should be able to enter expressions like 3 + 2i
. Note that real numbers should never be pretty-printed with an imaginary component, and purely imaginary numbers should not be pretty-printed with a real component. For example,
> 2
2.0
= 2.0
> 3i
3.0i
= 3.0i
> i + 2
i + 2.0
= 2.0 + i
> 2 + 3i
2.0 + 3.0i
= 2.0 + 3.0i
> (2 + 3i) * (4 + 6i)
(2.0 + 3.0i) * (4.0 + 6.0i)
= -10.0 + 24.0i
> sqrt(2 + 3i)
sqrt(2.0 + 3.0i)
= 1.6741492280355401 + 0.8959774761298381i
(The last example works only if you have also implemented the first extension.)
You can import the Complex.Double
module to work with complex numbers in Haskell.
Note there is a slight wrinkle to deal with when parsing a literal imaginary value: if you see a number you do not yet know whether it will be followed by i
or not. The problem is that by default, if a parsec parser consumes some input before failing, it does not backtrack to try re-parsing the same input. So, as an example, something like this:
Imag <$> (integer <* reserved "i") <|> Real <$> integer
does not work, since if there is an integer not followed by an i
, the first parser will irreversibly consume the integer before failing to find an i
; when the second parser is tried there will no longer be an integer for it to find.
The solution is that any parser which you would like to backtrack can be wrapped in the try
function. So
Imag <$> try (integer <* reserved "i") <|> Real <$> integer
works as expected: if there is no i
following an integer and the first parser fails, the input gets rewound to the beginning of the integer before trying the second parser.
Support for units of measurement. Pick a domain (e.g. length, mass, time, …) and allow the user to add units in that domain to their calculations. For example (yours does not have to work exactly like this):
> 1
1.0
= 1.0
> 1 inch
1.0 in
= 1.0 in
> 1 inch + 3 inches
1.0 in + 3.0 in
= 4.0 in
> 1 meter + 1 inch
1.0 m + 1.0 in
= 1.0254 m
> (1 meter + 1 inch) as inches
(1.0 m + 1.0 in) as in
= 40.370078740157474 in
> ((1.6 ft * 700 + 8.1 ft) / 2) as miles
((1.6 ft * 700.0 + 8.1 ft) / 2.0) as mi
= 0.10678412422360248 mi
> 5 feet * 2 meters
5.0 ft * 2.0 m
= Error: tried to multiply two values with units, namely 5.0 ft and 2.0 m
> 5 km + 6
5.0 km + 6.0
= Error: tried to add values with and without units, namely 5.0 km and 6.0
> (5 km) mi
5.0 km mi
= Error: tried to apply units mi to a value that already had units km
> (5 km) as mi
5.0 km as mi
= 3.105590062111801 mi
> 6 as cm
6.0 as cm
= Error: can't convert scalar 6.0 to cm
Some hints:
Support for simple algebraic expressions involving polynomials. For example:
> (x+1)^2
(x + 1)^2
= x^2 + 2*x + 1
> (x+1)*(y-3)
(x + 1) * (y - 3)
= x * y - 3 * x + y - 3
> (x^2 + 3*x + 1) / (x + 1)
Sorry, division of polynomials is not supported.
If you want to be really fancy you could support polynomial division too:
> (x^2 + 3*x + 1) / (x + 1)
(x^2 + 3 * x + 1) / (x + 1)
= x + 2 - 1 / (x + 1)
You should also feel free to propose your own extensions; just be sure to run them by me to make sure you choose something with an appropriate level of difficulty.