- Open
Quadratic.hs
and add a module definition to it:
module Quadratic where
- Import the standard module
Data.Complex
to help us represent a complex solution to the quadratic equation. - Define the data type to represent the quadratic equation:
data Quadratic = Quadratic { a :: Double, b :: Double,
c :: Double }
deriving Show
This represents the quadratic equation of the form a∗x2+b∗x+c=0a∗x2+b∗x+c=0
. a
, b
, and c
represent the corresponding constants in the equation.
- Define the data type for representing the root:
type RootT = Complex Double
This represents that the complex data type parameterized by Double
. RootT
is synonymous to type Complex Double
(similar to typedef
in C/C++).
- A quadratic equation has two roots, and hence we can represent both the roots as follows:
import Data.Complex
type RootT = Complex Double
data Roots = Roots RootT RootT deriving Show
- Implement the solution. We will take a top-down approach to create a solution. We will define a top-level function where we will implement a function assuming lower level details:
roots :: Quadratic -> Roots
This shows that the roots
function takes one argument of type Quadratic
, and returns Roots
.
- Implement three cases mentioned next:
-- | Calculates roots of a polynomial and return set of roots
roots :: Quadratic -> Roots
-- Trivial, all constants are zero, error roots are not defined
roots (Quadratic 0 0 _) = error "Not a quadratic polynomial"
-- Is a polynomial of degree 1, x = -c / b
roots (Quadratic 0.0 b c) = let root = ( (-c) / b :+ 0)
in Roots root root
-- b^2 - 4ac = 0
roots (Quadratic a b c) =
let discriminant = b * b - 4 * a * c
in rootsInternal (Quadratic a b c) discriminant
We have referred to the rootsInternal
function, which should handle case A, B, and C for the case III.
- Implement the
rootsInternal
function to find all roots of the quadratic equation:
rootsInternal :: Quadratic -> Double -> Roots
-- Discriminant is zero, roots are real
rootsInternal q d | d == 0 = let r = (-(b q) / 2.0 / (a q))
root = r :+ 0
in Roots root root
-- Discriminant is negative, roots are complex
rootsInternal q d | d < 0 = Roots (realpart :+ complexpart)
(realpart :+ (-complexpart))
where plusd = -d
twoa = 2.0 * (a q)
complexpart = (sqrt plusd) / twoa
realpart = - (b q) / twoa
-- discriminant is positive, all roots are real
rootsInternal q d = Roots (root1 :+ 0) (root2 :+ 0)
where plusd = -d
twoa = 2.0 * (a q)
dpart = (sqrt plusd) / twoa
prefix = - (b q) / twoa
root1 = prefix + dpart
root2 = prefix - dpart
Open src/Main.hs
. We will use the Quadratic
module here to solve a couple of quadratic equations. Add the following lines of code in Main.hs
:
module Main where
import Quadratic
import Data.Complex
main :: IO ()
main = do
putStrLn $ show $ roots (Quadratic 0 1 2)
putStrLn $ show $ roots (Quadratic 1 3 4)
putStrLn $ show $ roots (Quadratic 1 3 4)
putStrLn $ show $ roots (Quadratic 1 4 4)
putStrLn $ show $ roots (Quadratic 1 0 4)
- Execute the application by building the project using
stack build
and then executing with stack exec – quadratic-exe
in the command prompt. You will see the following output:
Roots ((-2.0) :+ 0.0) ((-2.0) :+ 0.0)
Roots ((-1.5) :+ 1.3228756555322954) ((-1.5) :+
(-1.3228756555322954))
Roots ((-1.5) :+ 1.3228756555322954) ((-1.5) :+
(-1.3228756555322954))
Roots ((-2.0) :+ 0.0) ((-2.0) :+ 0.0)
Roots ((-0.0) :+ 2.0) ((-0.0) :+ (-2.0))
A quadratic equation is represented by ax^2 + bx + c = 0
. There are three possible cases that we have to handle:
We will define a module at the top of the file with the Quadratic
module where the name of the module matches file name, and it starts with a capital letter. The Quadratic
module is followed by the definition of module (data types and functions therein). This exports all data types and functions to be used by importing the module.
We will import the standard Data.Complex
module. The modules can be nested. Many useful and important modules are defined in the base package. Every module automatically includes a predefined module called Prelude. The Prelude exports many standard modules and useful functions. For more information on base modules, refer to https://hackage.haskell.org/package/base.
The user-defined data is defined by the keyword data followed by the name of the data type. The data type name always start with a capital letter (for example, data Quadratic
).
Here, we will define Quadratic
as follows:
data Quadratic = Quadratic { a :: Double, b ::
Double, c :: Double }
deriving Show
There are several things to notice here:
- The name on the left-hand side,
Quadratic
, is called type constructor. It can take one or more data types. In this case, we have none. - The name
Quadratic
on the right-hand is called data constructor. It is used to create the value of the type defined on the left-hand side. {a :: Double, b :: Double, c :: Double }
is called the record syntax for defining fields. a
, b
and c
are fields, each of type Double
.- Each field is a function in itself that takes data type as the first argument and returns the value of the field. In the preceding case,
a
will have the function type Quadratic -> Double
, which means a
will take the value of type Quadratic
as the first argument and return the field a of type Double
. - The definition of data type is followed by deriving
Show
. Show
is a standard type class in Haskell and is used to convert the value to String
. In this case, Haskell can automatically generate the definition of Show
. However, it is also possible to write our own definition. Usually, the definition generated by Haskell is sufficient.
We will define root as type Complex Double
. The data type Complex
is defined in the module Data.Complex
, and its type constructor is parameterized by a type parameter a
. In fact, the Complex
type is defined as follows:
data Complex a = a :+ a
There are several things to notice here. First, the type constructor of Complex
takes an argument a
. This is called type argument, as the Complex
type can be constructed with any type a
.
The second thing to note is how the data constructor is defined. The data constructor's name is not alphanumeric, and it is allowed.
Note that the data constructor takes two parameters. In such a case, data constructor can be used with infix notation. That is, you can use the constructor in between two arguments.
The third thing to note is that the type parameter used in the type constructor can be used as a type while defining the data constructor.
Since our quadratic equation is defined in terms of Double
, the complex root will always have a type Complex Double
. Hence, we will define a type synonym using the following command:
type RootT = Complex Double
We will define two roots of the equation using the following command:
data Roots = Roots RootT RootT deriving Show
Here, we have not used the record syntax, but just decided to create two anonymous fields of type RootT
with data constructor Roots
.
The roots
function is defined as follows:
roots :: Quadratic -> Roots
It can be interpreted as the roots
function has a type Quadratic -> Roots
, which is a function that takes a value of type Quadratic
and returns a value of type Roots
:
- Pattern matching: We can write values by exploding data constructor in the function arguments. Haskell matches these values and then calls the definition on the right-hand side. In Haskell, we can separate the function definition using such matching. Here, we will use pattern matching to separate cases I, II, and III, defined in the preceding section. The case I can be matched with value (
Quadratic 0 0 _
) where the first two zeros match fields a
and b
, respectively. The last field is specified by _
, which means that we do not care about this value, and it should not be evaluated. - Raising an error: For the first case, we flag an error by using function error. The function error takes a string and has a signature (
error :: String -> a
) which means that it takes a String
and returns value of any type a
. Here, it raises an exception. - let .. in clause: In the case II as mentioned in the preceding section, we use
let ... in
clause.
let root = ( (-c) / b :+ 0)
in Roots root root
Here, the let
clause is used to bind identifiers (which always start with a lowercase letter; so do function names). The let
clause is followed by the in
clause. The in
clause has the expression that is the value of the let…in
clause. The in
expression can use identifiers defined in let
. Furthermore, let
can bind multiple identifiers and can define functions as well.
We defined rootsInternal
as a function to actually calculate the roots of a quadratic equation. The rootsInternal
function uses pattern guards. The pattern guards are explained as follows:
- Pattern guards: The pattern guards are conditions that are defined after a vertical bar
|
after the function arguments. The pattern guard defines a condition. If the condition is satisfied, then the expression on the right-hand side is evaluated:
rootsInternal q d | d == 0 = ...
In the preceding definition, d == 0
defines the pattern guard. If this condition is satisfied, then the function definition is bound to the expression on the right-hand side.
- where clause: The
rootsInternal
function also uses the where
clause. This is another form of the let…in
clause:
let <bindings>
in <expression>
It translates to the following lines of code:
<expression>
where
<bindings>
In Main.hs
, we will import the Quadratic
module and use the functions and data type defined in it. We will use the do syntax, which is used in conjunction with the IO
type, for printing to the console, reading from the console, and, in general, for interfacing with the outside world.
The putStrLn
function prints the string to the console. The function converts a value to a string. This is enabled because of auto-definition due to deriving Show
.
We will use a data constructor to create values of Quadratic
. We can simply specify all the fields in the order such as, Quadratic 1 3 4
, where a = 1
, b = 3
, and c = 4
. We can also specify the value of Quadratic
using record syntax, such as Quadratic { a = 10, b = 30, c = 5 }
.
Things are normally put in brackets, as shown here:
putStrLn (show (roots (Quadratic 0 1 2)))
However, in this case, we will use a special function $
, which simplifies the application of brackets and allows us to apply arguments to the function from right to left as shown:
putStrLn $ show $ roots (Quadratic 0 1 2)
You must have also noticed how the Haskell source code is formatted. The blocks are indented by white spaces. There is no hard and fast rule for indenting; however, it must be noted that there has to be a significant white space indent for a source code block, such as the let
clause or the where
clause. A simple guideline is that any block should be indented in such a way that it is left aligned and increases the readability of the code. A good Haskell source is easy to read.