Console Version
How would we write a calculator program in Haskell? A simple calculator should be able to perform the basic arithmetic operations on two numbers.
data Operator = Add | Subtract | Multiply | Divide
deriving Read
eval l o r = case o of
Add -> l + r
Subtract -> l - r
Multiply -> l * r
Divide -> l / r
prompt txt = do
putStrLn txt
readLn
main = do
l <- prompt "Left operand?"
o <- prompt "Which operator?"
r <- prompt "Right operand?"
putStrLn $ "The result is " ++ show (eval l o r)
Note that since we used deriving Read
, we get an instance of the Read
typeclass. That means that the read
function now has a default implementation for our datatype Operator
. This default implementation expects the input to be formatted in the same way that show
formats its output. In this case, that means it expects one of "Add", "Subtract", "Multiply", or "Divide".
But something interesting is going on! prompt "Left operand"
and prompt "Right operand"
are very similar expressions, but one reads numbers while the other reads operators. The type of read is Read a => String -> a
. This means that read
can be used on a String
, to return any type a
, that implements the Read
typeclass.
main = do
value <- readLn
print (value :: Int)
Try changing the type of value
to String
, [Int]
, Bool
, etc. If you remove it, there will be an "ambiguous type variable" exception - this is the compiler telling you that it needs more information in order to determine the type. Without knowing the type, Haskell can't determine how to readLn
(which uses read
) or print
(which uses show
).
Yesod Version
Let's make our calculator program into a website! Here's our pre-requisites:
{-# LANGUAGE TypeFamilies, QuasiQuotes, MultiParamTypeClasses,
TemplateHaskell, OverloadedStrings #-}
import Yesod
import Yesod.Form
import Control.Applicative
Nevermind the LANGUAGE
stuff. Beyond that, we're depending on Yesod and its forms. The operators from Control.Applicative
are needed to build forms.
Next, we write a simple one-page website:
data Calculator = Calculator
instance Yesod Calculator
-- Necessary to tell Yesod how to render form-related error messages.
instance RenderMessage Calculator FormMessage where
renderMessage _ _ = defaultFormMessage
mkYesod "Calculator" [parseRoutes|
/ HomeR GET
|]
main = warpEnv Calculator
The code for the form looks like this:
data Calculation = Calculation Double Operator Double
form = renderDivs $ Calculation
<$> areq doubleField "Left operand" Nothing
<*> areq (selectField optionsEnum) "Operator" Nothing
<*> areq doubleField "Right operand" Nothing
See the Yesod Book for more information about forms. In this example areq
builds a required field. doubleField
and selectField optionsEnum
specify different types of forms widgets (as well as a way of parsing the results).
The operators (<$>)
and (<*>)
from Control.Applicative
allow us to treat the fields as if they yield values, and wrap up those results using a Calculation
datatype.
Finally, we can define our page handler and use logic code that's similar to the console version above:
{-# LANGUAGE TypeFamilies, QuasiQuotes, MultiParamTypeClasses,
TemplateHaskell, OverloadedStrings #-}
import Yesod
import Yesod.Form
import Control.Applicative
data Calculator = Calculator
instance Yesod Calculator
instance RenderMessage Calculator FormMessage where
renderMessage _ _ = defaultFormMessage
mkYesod "Calculator" [parseRoutes|
/ HomeR GET
|]
main = warpEnv Calculator
-- show
data Calculation = Calculation Double Operator Double
form = renderDivs $ Calculation
<$> areq doubleField "Left operand" Nothing
<*> areq (selectField optionsEnum) "Operator" Nothing
<*> areq doubleField "Right operand" Nothing
getHomeR :: GHandler sub Calculator RepHtml
getHomeR = do
((result, widget), enctype) <- runFormGet form
let resultText = "Result: " ++ case result of
FormSuccess (Calculation l o r) -> show (eval l o r)
_ -> ""
defaultLayout $ do
[whamlet|
<h1> Calculator </h1>
<form enctype=#{enctype}>
^{widget}
<input #button type="submit" value="Calculate!">
<h2> #{toHtml resultText}
|]
toWidget [cassius|
body { width: 15em; margin: 0em auto; }
#button { width: 10em; }
input, select { width: 4em; margin: 0.5em; }
.required { text-align: right; }
|]
data Operator = Add | Subtract | Multiply | Divide
deriving (Eq, Enum, Bounded, Show)
eval l o r = case o of
Add -> l + r
Subtract -> l - r
Multiply -> l * r
Divide -> l / r
The logic code has one small difference - now, instead of deriving Read
, we do deriving (Eq, Enum, Bounded, Show)
. This is because these instances are needed by selectField optionsEnum
. Eq
is needed to determine which of the options was selected. Bounded
and Enum
are used to enumerate all the possible values of Operator
. Show
is used to determine the label used in the drop-down box entry.