User:Davjam2/Numbers
Note
This is a draft of a new page to add to the Haskell Wikibook.
I think (hope) it's worth writing. I was very confused when I first tried to do any numerical stuff in Haskell, even though I had be programming in Haskell for a few years by then and was already quite comfortable with types and typeclasses.
I do have some concerns:
- is it too long? (should it be divided, or some sections separated out)?
- there is overlap/duplication with existing content
- is it too GHC specific?
- is unsafePerformIO OK as used? It looks controversial (e.g. here). Is there a better way to catch the errors without it than my monadic mess
putOpTableE
?
Quite a bit is said about numbers in the Type basics II and Classes and types pages. Those pages use the numeric types and classes in Haskell to illustrate the Haskell type system. This page is more focussed on numbers themselves: how to do different types of numeric computations using the Haskell types and classes.
Numbers
[edit | edit source]Mathematically, numbers can be classified into different sets, such as the integers (), the rational numbers () and the real numbers (). It is common to say that the integers are a subset of the rational numbers, and that the rational number 5 "is an integer". Some operations, such as division, are only possible within certain sets of numbers: there is no solution to amongst the integers, but there is amongst the rational numbers. Rational numbers are said to be closed under division (excluding division by zero), but integers are not.
No programming language (not even Haskell) has a way of representing "any number". Generally, a language will have different numeric types, each representing (possibly a subset of) the numbers in one of the classification sets. In Haskell, there are types such as Integer
, Rational
and Float
that closely represent , and a subset of . Prelude provides a number of these types, and more are available in other modules. None of these types is perfect in all situations: for example, none of the standard types can represent or precisely.
In Haskell, the 5 :: Integer
and 5 :: Rational
are definitely not the same thing. It's not even possible to compare them directly using (==)
, since (==)
can only compare values of the same type. Many programming languages allow operations on numbers of different types (such as adding the integer 5 to the number 3.14). They usually do this by quietly "upgrading" one of the numbers so that they are both of the same types before applying the operation. Haskell does not directly allow addition of different types of number, nor does it quietly "upgrade" numbers to different types. It does, however, allow explicit conversion between types.
Most standard numeric operations are defined as class methods, so that they work on all of the relevant numeric types. For example: (+)
(addition) is defined to work on all numeric types; (/)
(division, which can find the solution to ) is not available for integer types; and sin
(finding the sine of an angle) will work on types representing (as well as they can) real numbers. A smaller number of mathematical operations (e.g. gcd
) are defined as functions (which call the class methods). Many of the numerical functions (class methods or not) are partial, and will throw exceptions for certain parameters (for example division by zero, for some types). You should either ensure your code avoids such parameters, or handles the exceptions that may occur.
Numeric Types
[edit | edit source]Some of the Haskell numeric types are:
Set | Type | Module | Values | Example | |
---|---|---|---|---|---|
[1] | Natural |
Numeric.Natural |
unbounded unsigned integers | 2158269056624017538838
| |
Word8 , Word16 , etc |
Data.Word |
unsigned integers that can be stored in 8, 16, etc, computer bits | 252
| ||
Word |
unsigned integers, based on the size of the computer's register[2] | 4294967242
| |||
Integer |
unbounded signed integers | 2158269056624017538838
| |||
Int8 , Int16 , etc |
Data.Int |
signed integers that can be stored in 8, 16, etc, computer bits | 126
| ||
Int |
signed integers, based on the size of the computer's register[2] | 2147483621
| |||
Rational [3] |
a ratio of two Integer s |
2158269056624017538838 % 5
| |||
Ratio Int |
Data.Ratio |
a ratio of two Int s |
22 % 7 (, or )
| ||
Deci , Centi , Milli , etc |
Data.Fixed |
unbounded signed numbers with a fixed number of decimal places | 271.20
| ||
Float |
IEEE single-precision floating point numbers (about 8 significant digits) | 3.1415927
| |||
Double |
IEEE double-precision floating point numbers (about 16 significant digits) | 3.141592653589793
| |||
Complex Float |
Data.Complex |
complex numbers whose real and imaginary parts are both represented by Float s. |
3.29 :+ 2.5 ()
|
The set shown against a type is the "smallest" of those sets illustrated above that contains all of the numeric values of that type. The types Natural
, Integer
and Rational
can represent all of the values in the corresponding sets, subject to the computer's memory and performance limits. The other types can represent only subsets of the corresponding sets, for example Int8
can only represent integers in the range -128 to 127, and Float
can only represent those real numbers within the (approximate) range –3.4028235 × 1038 to –3.4028235 × 1038 that can be written with about 8 significant digits.
In mathematics, any value in a "smaller" set has an equivalent in a "larger" set. For example, , and all integers have an equivalent real number. In Haskell types, this relationship is only partially preserved: 5 :: Integer
has an equivalent 5 :: Float
, but 9876543210987654321 :: Integer
does not have a corresponding value of type Float
(since Float
does not represent all real numbers).
The indicates types that include values (such as "infinity" and "negative zero") in addition to those in the corresponding sets.
A blank in the Module column means the type is exported by Prelude.
Some of the numeric types (Ratio
and Complex
) are parametized types, i.e. you can't create a value of type Complex
, but you can create a value of type Complex Float
, Complex Double
, etc[4].
There are some type-specific functions, for example numerator
, which returns the numerator of a Ratio
.
There are many more numeric types than listed in the table above, and the set of numeric types can be extended:
- you can create your own―something we will do later in this page; and
- you can search Hoogle for other libraries that provide types such as matrices, polynomials[5], etc.
Exercises |
---|
|
What Numeric Means (The Num
Class)
[edit | edit source]By "numeric", we mean that the types support some basic maths such as addition and multiplication. In Haskell, these basic maths operations are defined in the Num
class. The types listed above all are instances of Num
, and each provides an appropriate implementation for them.
Note the type of the addition operator:
GHCi> :t (+) (+) :: Num a => a -> a -> a
As explained previously, this type signature shows that (+)
is a function[6] that takes two parameters (of the same type), and returns a value (of that same type). But the type can't be any of type (it can't be a Bool
, for example), it has to be a type that's an instance of the class Num
. Hence (+)
is polymorphic: it can operate on different types (at different times; not in the same expression), subject to the Num
constraint. (We will talk about how to calculate the sum of numbers of different types in the section on numeric conversions later.)
GHCi> (123 :: Integer) + (5 :: Integer) 128 GHCi> :t (123 :: Integer) + (5 :: Integer) (123 :: Integer) + (5 :: Integer) :: Integer
Note
I've explicitly stated the types of both of the numbers in this example for clarity. This is not something you'd normally do (although you may have to or want to occasionally). If I had just typed 123 + 5
how would GHC know which type I meant? It could be any of the types in the table above. We'll see the answer in the sections on numeric literals and defaults later.
If I'd entered 123 + 5 :: Integer
this would also imply that 123
and 5
are Integer
values. This is because I'm declaring that the result of the (+)
should be an Integer
, which (by the type signature) would require the type of each of the arguments to be Integer
as well. This is a little easier on the eye, and is what I will do in the following examples.
Num
also includes functions for multiplication and subtraction (and some more; we will see the details later). It does not provide division. There are additional numeric classes that provide functions that are only suitable for a subset of the numeric types, for example:
sin
, which returns the sine of an angle, where it doesn't make sense to return any of the integral types; and(/)
anddiv
, which do different types of division suitable for different types of number.
Num
also includes the function fromInteger
, which creates a value of any numeric type from an integer (e.g. fromInteger 5 :: Complex Float
gives 5.0 :+ 0.0
). This is one of a number of type conversion functions discussed in more detail later. It is also used by the Haskell when compiling a numeric literal.
All of the numeric types above are instances of Eq
, though this should be used with care with types representing approximations (e.g. Float
). Many are also instances of other standard classes (Ord
, Enum
and Bounded
), but there are nuances in their behaviour.
Exercises |
---|
|
Comparison of Types
[edit | edit source]Accuracy and Precision
[edit | edit source]Some of these types (e.g. Integer
) offer complete accuracy. You can multiply two 50-digit Integer
values together and get a mathematically exact 100-digit result. Rational
is also completely accurate.
Some of the types (e.g. Int8
, Ratio Int
) are completely accurate, provided the numbers stay within the range supported by the type. (Note that operations that cause overflow typically don't raise exceptions).
Some types (e.g. Float
and Double
) are used for recording approximations. Double
offers greater precision that Float
, but neither can represent the exact value of , for example. They also suffer from rounding errors, e.g:
> 0.12 + 0.03 :: Float 0.14999999 > 0.12 + 0.03 == (1.15 :: Float) False
As a result, for approximation types:
- it probably makes sense to round values as required prior to display to a user (i.e. don't use the result of
show
); and - it probably doesn't make sense to compare them for equality.
Exercises |
---|
|
Overflow
[edit | edit source]In general, mathematical operations in Haskell do not raise overflow exceptions. The actual behaviour differs amongst the types.
TODO: Fix the statements about modulo behaviour for integral types since they may not be correct. The Haskell report says the overflow behaviour is undefined, and it is possible that it is different in different implementations (e.g. on LLVM).
For bounded integral types, values "wrap" (i.e. the arithmetic is performed modulo 2n, where n is the number of bits in the type):
> 127 + 1 :: Int8 -128
If overflow is likely, you should consider the options, for example:
- use a different type (e.g.
Integer
instead ofInt
, where overflow is not possible); or - if a different type is not possible (e.g. it's an integer that needs to be stored in some fixed size field in a database), maybe use a numeric type from a library that does raise exceptions on overflow (or use e.g.
Integer
but do validation prior to trying to store the result); or - be happy that it's what you wanted (e.g. for calculating a checksum of a file).
Note that succ
does raise an exception for a bounded integral type when exceeding a bound:
> succ 127 :: Int8 *** Exception: Enum.succ{Int8}: tried to take `succ' of maxBound
Natural
can raise an exception during subtraction:
> 4 - 7 :: Natural *** Exception: arithmetic underflow
When floating point types overflow, they take the value Infinity
:
> 1.0e23 * 1.0e23 :: Float Infinity
Exercises |
---|
|
Division by Zero
[edit | edit source]Division by zero will raise an exception for some types, but for Float
and Double
, a value Infinity
is returned:
> 6 / 0 :: Rational *** Exception: Ratio has zero denominator > 6 `div` 0 :: Integer *** Exception: divide by zero > 6 / 0 :: Float Infinity
However, it is possible to create Rational
values with zero denominators by directly using the (:%)
constructor or utility functions:
> 5 :% 0 :: Ratio Word8 5 % 0 > infinity 1 % 0 > notANumber 0 % 0
Note that Float
and Double
also support negative infinity, negative zero, and NaN ("not a number"):
> let pinf = 6 / 0 :: Float > let pz = 12 / pinf > let ninf = -6 / 0 :: Float > let nz = 12 / ninf > ninf -Infinity > pz 0.0 > nz -0.0 > pz == nz True > (nz + 1) - 1 0.0 > pz / pz NaN > pinf / pinf NaN
They will also return NaN
for e.g. sqrt (-1)
.
They support functions isInfinite
and isNaN
so you can check for potential errors.
Exercises |
---|
|
Enumerations
[edit | edit source]Many of the numeric types are instances of Enum
, including fractional types. The Enum
class includes methods toEnum
and fromEnum
for converting between values of the type and values of the (bounded) type Int
. Most types (including most numeric types) will not have a 1:1 mapping with Int
, leading to some interesting behaviour.
There is no requirement that toEnum n
represents the numeric value n, though this is the case for many types:
> toEnum 127 :: Int8 127 > toEnum 127 :: Float 127.0 > toEnum 127 :: Centi 1.27
In some cases where n is out of bounds, fromIntegral n
will cause "wrapping" but toEnum n
will raise an exception:
> fromIntegral 128 :: Int8 -128 > toEnum 128 :: Int8 *** Exception: Enum.toEnum{Int8}: tag (128) is outside of bounds (-128,127)
fromEnum
will have corresponding behaviour, and in some cases it truncates fractional parts:
> fromEnum (3.1415927 :: Float) 3 > fromEnum (22 % 7 :: Ratio Int) 3 > fromEnum (1.27 :: Centi) 127
Where the result is out of bounds for an Int
, typically "wrapping" will occur without raising exceptions:
> fromEnum (2158269056624017538838 :: Integer) -234 > fromEnum (3.1415927e22 :: Float) 1121396307215253504
succ
and pred
are consistent with the above. In many cases, succ
has the same effect as adding 1 (but not for Deci
etc). As mentioned above, succ
may raise an exception when exceeding the bounds of a type. For some types succ
will work correctly even though fromEnum
"wraps":
> succ 2158269056624017538838 :: Integer 2158269056624017538839
For imprecise floating-point types, succ
will make no difference to large values due to lost precision:
> succ 1.0e23 :: Float 1.0e23
Enum
also includes methods enumFrom
and variations. Haskell compiles [x..]
, [x,y..z]
, etc, into these.
Exercises |
---|
|
Internal Representation
[edit | edit source]Most numeric types provide functions that give access to their internal representations.
Some numeric types are made of separate components, for example Rational
has two Integer
components, and Complex Double
has two Double
components. Both types have functions e.g. numerator
or realPart
, that access these.
Complex
exposes its infix constructor :+
, which can be used for creating complex numbers, pattern matching, etc. Ratio
does not expose its constructor (it's :%
, hidden in GHC.Real
). %
is not a constructor, it's an operator that first reduces the ratio (e.g. to ) and then creates the Ratio. In addition, the show
method displays %
.
The bits representation of integers can be accessed via Data.Bits
module, for example:
> finiteBitSize (0 :: Word8) 8 > bit 4 .|. bit 1 :: Word8 18 > countLeadingZeros (18 :: Word8) 3 > shift 18 1 36
There are also functions such as decodeFloat
for accessing details of floating point numbers.
Exercises |
---|
|
Type Classes
[edit | edit source]As stated above, most standard numeric operations are defined as class methods, so that they can be applied to all appropriate types. The numeric type classes in Prelude are:
Class | Example Methods | Means | Doesn't mean |
---|---|---|---|
Num |
(+) , (*) |
any numeric type (i.e. supporting basic mathematical operations) | A type that can only represent "numbers". For example, a type representing a matrix might be an instance |
Fractional |
(/) , recip |
a type that includes non-integral values | a type that can only be represented as a fraction (i.e. a rational number). For example, a type that could represent might be an instance |
Floating |
pi , sin |
a Fractional type that approximates a numeric value to a certain precision |
Only real numbers. For example, Complex Float is an instance.
|
Real |
a type representing (a subset of) real (i.e. not complex) numbers. Real types must also be instances of Ord |
a type that can represent any real number. (Most instances can't represent ) | |
RealFrac |
round , floor |
a type that's both Real and Fractional |
|
RealFloat |
isInfinite |
a type that's both Real and Floating |
|
Integral |
div |
a Real type that can represent only integer values. Integral types must also be instances of Enum |
a type that can represent any integer value (many Integral types can only represent a certain range of integers)
|
The classes form a hierarchy (shown here), so that a type can't be an instance of, say Real
if it's not also an instance of Num
.
Exercises |
---|
|
Numeric Conversions
[edit | edit source]Introduction
[edit | edit source]Suppose we want a function that can add any Integer
value to . Let's try:
nPlusPi :: Integer -> Float
nPlusPi n = n + pi
Oops - we get an error: Error: Couldn't match expected type ‘Float’ with actual type ‘Integer’
. This makes sense: the definition of (+)
requires both types to be the same.
How do we fix this? Using the fromInteger
method on the Num
class:
nPlusPi :: Integer -> Float
nPlusPi n = fromInteger n + pi
fromInteger
is one of a number of numeric conversion functions that we'll look at.
However, before looking at them, note that there aren't separate floatFromInteger
, doubleFromInteger
, rationalFromInteger
, etc, functions. Instead, fromInteger
is a method on the class Num
, and each instance provides its own implementation[7]. When fromInteger
is called, the function that calls it needs to be clear which type should be returned[8]. In the above example, the required type was determined by type inference, based on the types of nPlusPi
and (+)
[9].
Note
Many other languages allow addition of mixed numeric types, and will "quietly" convert them so that they can be added together. This can make it easier to write some code, but can also lead to subtle run-time errors.
Exercises |
---|
|
More Integral Conversions
[edit | edit source]The diagram on the right shows fromInteger
in a class hierarchy diagram, along with a number of other conversion functions. It is a method on the Num
class, so can create a value of any numeric type from a Integer
value.
Another method shown is toInteger
. This is a method on the Integral
class, so any Integral
type (e.g. Int
, Word8
, and even Integer
itself) provides a method to create an Integer
with the equivalent numeric value. Prelude also includes a function fromIntegral
that combines toInteger
and fromInteger
, making it easy to convert from any Integral
type to any numeric type.
Exercises |
---|
|
Fractional Conversions
[edit | edit source]Two class methods support the majority of fractional conversions:
fromRational :: Fractional a => Rational -> a
toRational :: Real a => a -> Rational
fromRational
is a method of the Fractional
class, so it's possible to create a Fractional
of any type from a Rational
. As with fromInteger
, the function that calls fromRational
needs to be clear what type should be returned.
Just as with nPlusPi
, we have a type problem if we try to write this:
xPlusApproxPi :: Float -> Rational
xPlusApproxPi x = x + 22%7
since we're trying to add a Float
to a Rational
.
toRational
is a method of the Real
class, so will work for e.g. Int
, Integer
, Float
, etc, and even on Rational
itself. Each type instance will define its own implementation, and is expected to return "the rational equivalent of its real argument with full precision". For approximate types (e.g. Float
) the result should therefore be an exact equivalent of the approximate value.
There is also the function realToFrac
, that is a combination of toRational
and fromRational
, which can convert any instance of Real
to any instance of Fractional
.
Note that, with Float
and Double
, toRational
loses the negative of negative zeros and "rounds" infinities to large fractions. This impacts how realToFrac
handles conversions between them. The functions floatToDouble
and doubleToFloat
in GHC.Float
avoid these problems:
> realToFrac (-0.0 :: Float) :: Double 0.0 > float2Double (-0.0) -0.0 > realToFrac (5 / 0 :: Float) :: Double 3.402823669209385e38 > float2Double (5 / 0) Infinity
Exercises |
---|
|
Rounding
[edit | edit source]toInteger
(and hence fromIntegral
) will not work on, say, pi
, since there is no way to return a value equivalent to pi
as an Integer
. However, there are functions such as round
, ceiling
, etc. These are methods on the RealFrac
class that will return a value of any Integral
type:
floor :: (RealFrac a, Integral b) => a -> b
ceiling :: (RealFrac a, Integral b) => a -> b
Since there's no integer that's a numeric equivalent of pi
, these functions give different options for the conversion:
> floor pi 3 > ceiling pi 4
RealFrac
also provides the method properFraction
, which decomposes a number into an integral and fractional parts, e.g:
> properFraction pi (3,0.14159265358979312) > properFraction (22%7) (3,1 % 7)
length
, etc, and generic versions
[edit | edit source]Prelude includes functions such as length
and take
that return or take values only of type Int
. You may think "well, I'd never need to use a huge number such as available in Integer
, so Int
is good enough." But remember there are other Integral
types as well, such as Word8
, etc, and it might sometimes be useful to get/give those as parameters.
Hence the module Data.List
also exports "generic" versions of the functions:
genericLength :: Num i => [a] -> i
genericTake :: Integral i => i -> [a] -> [a]
Note that genericLength
can return any Num
, although it will always return a value equivalent to an integer (e.g. 12.0 :: Float
).
Exercises |
---|
|
Converting to/from Strings
[edit | edit source]There are a number of functions for converting numbers between numbers and strings. show
and read
are some, but they only provide limited behaviour and may well not be adequate for many purposes.
Some other functions include showIntAtBase
(which can show an integer value in any base), and showEFloat
(which has an option to specify the number of decimal places). These functions return a type ShowS
, which makes them more efficient when building large strings.
> showIntAtBase 3 intToDigit 11 "" "102" showFFloat (Just 2) (pi*10000) "" "31415.93"
There is also the function printf
in the Text.Printf
module which can make it easier to embed formatted numbers within longer strings, for example:
> printf "The values are %s, %d and %.4f.\n" "hello" 123 pi The values are hello, 123 and 3.1416.
Beware that printf
can't check that the parameter types against the format string at compile time, and will raise a run-time exception if they mismatch.
Exercises |
---|
|
Expression Syntax
[edit | edit source]Operators, Sections
[edit | edit source]Many mathematical functions are operators, which are often written in infix notation, e.g. 18/4
, but can be written prefix, e.g. (/) 18 4
. Non-operator functions are often written in prefix, e.g. div 18 4
, but can be written infix, e.g. 18 `div` 4
.
Operators can be used in sections, e.g. map (/4) [18, 20, 22]
, which is equal to [18/4, 20/4, 22/4]
, and map (4/) [18, 20, 22]
, which is equal to [4/18, 4/20, 4/22]
. Functions written in infix can also be used as sections, e.g. map (`div` 4) [18, 20, 22]
or map (400 `div`) [18, 20, 22]
.
Exercises |
---|
|
Fixity, Precedence
[edit | edit source]What is meant by the mathematical expression 5 + 32 × 4? There is a convention that we apply the operations in a certain order, like this: 5 + 32 × 4 = 5 + 9 × 4 = 5 + 36 = 41. The convention says that the exponentiation (raising to a power) operator has a higher precedence than multiplication, which in turn has a higher precedence than addition. If we wanted to add the 5 and 3 together first, we'd indicate it using brackets: (5 + 3)2 × 4 = 82 × 4 = 64 × 4 = 256.
What about the expression 7 − 3 − 2? The convention is that subtraction associates to the left, like this: 7 − 3 − 2 = 4 − 2 = 2. . If we wanted the 3 − 2 to happen first, we'd also need to indicate it using brackets: 7 − (3 − 2) = 7 − 1 = 6.
Haskell is aware of the conventions and the use of brackets:
> 5 + 3 ^ 2 * 4 41 > (5 + 3) ^ 2 * 4 256 > 7 - 3 - 2 2 > 7 - (3 - 2) 6
In a Haskell program, the precedence and associativity of operators can be specified using "fixity" declarations. For example, (-)
and (^)
have these declarations:
infixl 6 -
infixr 8 ^
This shows that (^)
has a higher precedence than (-)
, that (-)
associates to the left and that (^)
associates to the right[10]. The standard mathematical operators follow the standard conventions[11], and are part of a precedence hierarchy that includes other operators, as shown here.
Fixity declarations can also be given for functions, and are used when those functions are written in infix style, e.g.:
infixl 7 `div`
Hence 3 + 50 `div` 3 `div` 2
means the same as 3 + ((50 `div` 3) `div` 2)
.
Note that prefix function application has the highest precedence, so succ 3 * 4
means (succ 3) * 4
, not succ (3 * 4)
.
Exercises |
---|
|
-
(negate) infix and prefix
[edit | edit source]The -
operator can be used normally as an infix operator. However, uniquely, it can also be used as an (unbracketed) prefix operator, where it has the same meaning as negate
[12]. This can be very useful, but can also be a bit confusing at times.
These expressions are OK, and -
has the two-parameter subtraction meaning:
8 - 5
(normal infix operator, giving 3)(-) 8 5
(normal prefix operator, giving 3)map (3 -) [1,2]
(normal section, means[3-1,3-2]
).2 * 3 - 5
(means(2 * 3) - 5
)2 == 3 - 5
(means2 == (3 - 5)
)
These expressions are OK, and -
has the one-parameter negate
meaning:
8 == - 3
(means8 == (negate 3)
, since-
has a higher precedence than==
)8 + (- 3)
(means8 + (negate 3)
)- 2 ^ 2
(meansnegate (2 ^ 2)
).- 2 == 2
(means(negate 2) == 2
).succ (- 3)
(meanssucc (negate 3)
)-(5 + 3)
(meansnegate (5 + 3)
)
These are not OK:
8 * - 3
and8 + - 3
. These don't compile, since the negate operator has lower (or the same) precedence as the other operator.succ - 3
. This means(succ negate) 3
, since function application has highest precedence, which doesn't compile.map (- 3) [8,9]
. Due the the special-
prefix operator,(- 3)
is not treated as a section. Instead it's interpreted asnegate 3
, which is not a function (and so can't be passed tomap
). However, you can use thesubtract
function instead:map (subtract 3) [8,9]
which gives[5,6]
.
Note that removing spaces makes no difference. For example 8 * -3
fails in exactly the same way as 8 * - 3
, despite it being "obvious to everyone that it should mean 8 * (-3)
".
You may want to put brackets around any usage of -
as a prefix operator for clarity. (And your own sanity).
Numeric Literals
[edit | edit source]Integer Literals
[edit | edit source]Suppose we want to define a Float
variable with the value . We can do this:
onePlusPi :: Float
onePlusPi = 1 + pi
OK, it only gives an approximation, but it's otherwise brilliant:
> onePlusPi 4.141593
But how does this work? Why don't we get a type error? onePlusPi
is defined to return a Float
. So (+)
must return a Float
. So each of the parameters to (+)
must be a Float
. pi
is defined so it can be a value of any instance of Fractional
, one of which is Float
, so it's happy. 1
however looks like an integer, which is not a Float
, so how does that work?
The answer is that the 1
actually compiles to fromInteger 1
, so onePlusPi
is essentially:
onePlusPi = fromInteger 1 + pi
fromInteger
is has type signature:
fromInteger :: Num a => Integer -> a
which can create a value of any numeric type we want. And right now we want a Float
(so we can add it to another Float
giving a Float
), so that's what we get.
We can see the effect of the usage of fromInteger
here:
> :t fromInteger fromInteger :: Num a => Integer -> a > :t fromInteger 1 fromInteger 1 :: Num a => a > :t 1 1 :: Num p => p[13]
So now we see why a simple number (e.g. 1
) has the weird type (Num p => p
): because it can be any type of number you want it to be.
If you do the Mod3
exercise below, you should have created a fromInteger
implementation for Mod3
, so can do:
> 2 :: Mod3 Two
Note that just because Haskell can interpret a numeric literal in code, doesn't mean read
will interpret it in the same way (or at all):
> read "2" :: Mod3 *** Exception: Prelude.read: no parse
Although it can if you also implement a Read
instance.
Fractional Literals
[edit | edit source]Numeric literals containing decimal points or the e
symbol are compiled using fromRational
, so you can write:
halfPlusApproxPi :: Rational
halfPlusApproxPi = 0.5 + 22%7
where 0.5 "looks like" a floating-point number, but means effectively:
halfPlusApproxPi = fromRational (1%2) + 22%7
where the decimal number 0.5 has been converted to and wrapped with fromRational
. We can see the effect of fromRational
here:
> :t fromRational fromRational :: Fractional a => Rational -> a > :t fromRational 0.5 fromRational 0.5 :: Fractional a => a > :t 0.5 0.5 :: Fractional p => p > 0.5 :: Rational 1 % 2 > 0.5 :: Float 0.5 > 0.5 :: Complex Float 0.5 :+ 0.0
Note that to be a fractional literal, digits are needed both before and after the decimal point. (i.e. 0.5
not .5
).
Literals in exponent form are compiled using fromRational
, even if they don't contain a decimal point, and even if they are integer values:
> :t 1.22583e3 1.22583e3 :: Fractional p => p > :t 1e3 1e3 :: Fractional p => p > 1e3 :: Float 1000.0 > 1e3 :: Int error: No instance for (Fractional Int) arising from ...
GHC has a language extension NumDecimals
that allows can allows 1e3 :: Int
.
Negative Literals
[edit | edit source]A "negative literal" compiles to an application of the prefix -
operator. Hence -7
is equivalent to negate (fromInteger 7)
, and -0.5
is equivalent to negate (fromRational (1%2))
.
In some cases, brackets may be needed due to operator precedence. For example 4 + -3
doesn't compile, but 4 == -3
does. 4 + (-3)
and -3 + 4
also compile correctly.
GHC has a language extension NegativeLiterals
that changes the interpretation, so that -7
is equivalent to fromInteger (-7)
.
Pattern Matching
[edit | edit source]Pattern matching on literals (numeric, character or string) compiles into a use of (==)
, so also depends on the numeric type being an instance of Eq
. E.g:
isZero :: Integer -> Bool
isZero 0 = True
isZero _ = False
compiles to:
isZero :: Integer -> Bool
isZero x | x == fromInteger 0 = True
| otherwise = False
Note that the following compiles without warning, but will never return 2:
f :: Float -> Int
f (-0) = 1
f 0 = 2
f _ = 3
Exercises |
---|
|
Non-Decimal literals
[edit | edit source]It is also possible to type integer literals in octal or hexadecimal:
> 0o77 63 > 0xFF 255 > 0xff 255
which are also all compiled using fromInteger
.
Fractional literals must be in decimal, but GHC has a language extension HexFloatLiterals
that also allows them in hexadecimal. 0x0.1
is then the same as 1/16
.
Defaults
[edit | edit source]Type Ambiguity
[edit | edit source]In some cases there is ambiguity in the type of an expression. In many cases, ambiguous expressions will not compile, for example:
e0 = toEnum 0
without the type signature is an error. You could want a Bool
, an Ordering
, an Integer
, but how does the compiler know?
To fix this, you can specify the type (ideally give e0
a type signature), or add an expression that uses e0
and requires it to be a specify type, e.g:
e1 = e0 == True
This expression would fix e0
to have type Bool
. (Alternatively I could write e.g. e2 = e0 == GT
, which would require e0
to have type Ordering
. But I can't have both expressions, since e0
can only have one type.)
In some cases, it's not possible to fix ambiguity using a top-level type signature. For example:
e3 :: Bool
e3 = toEnum 4 > toEnum 3
Here I've given e3
a type signature, but it's still ambiguous what types toEnum
should create. Here's one way to fix it[14]:
e3 :: Bool
e3 = (toEnum 4 :: Word8) > toEnum 3
These examples above are fairly contrived to make a point. But the problem does often arise, especially with numeric types. For example:
evenish :: Float -> Bool
evenish = even . round
which takes a Float
, and returns True
if it's closer to an even number than an odd number.
Note the types:
round :: (RealFrac a, Integral b) => a -> b
even :: Integral a => a -> Bool
Hence round
can produce any Integral
type and even
can consume any Integral
type. How is the compiler meant to know which of the many Integral
types to use? And why don't we get a type ambiguity error? The answer is defaults.
default
[edit | edit source]Each module (file) can have a default
code declaration, and if it's omitted it's assumed to be:
default (Integer, Double)
The details of exactly how it works are specified in the Haskell Report, but essentially means that instead of giving an error for some ambiguous types, it will use one of the defaults if possible. It will use Integer
for ambiguous Integral
types and Double
for ambiguous Fractional
types. Hence evenish
will compile so that round
will produce, and even
will consume, a value of type Integer
.
If you turn on e.g. {-# OPTIONS -Wall #-}
, which I often do, you will get a warning when type defaulting occurs.
You can change the defaulting rules by adding your own default
statement to the .hs file. You can even disable it by stating default ()
, in which case evenish
would give an error.
Exercises |
---|
|
Note that fromInteger
(hence also fromIntegral
) and fromRational
(hence also realToFrac
) are often subject to defaulting, since (by design) they can return a value of any one of a number of types. As a result, numeric literals (which compile using fromInteger
or fromRational
) are also often subject to defaulting.
default in GHCi
[edit | edit source]It is a little curious that, when you enter e.g. 125 + 10
into GHCi, it replies with 135
. Observe:
> 125 + 10 135 > it :: Int8 -121 > :t it it :: Num a => a
We can see that GHCi is being as helpful as possible. It does the sum initially using the default type (Integer
) because we didn't tell it otherwise (and displaying 135 is probably more helpful than displaying a type ambiguity error). If, however, we specify a type, it will do the calculation using the requested type, which in this case results in a different value.
Also, when we ask it to tell us the type of an expression, it will do so without regard to any defaulting that might occur upon evaluation.
We can turn off defaulting in GHCi as in .hs files:
> default () > 125 error: Ambiguous type variable...
In standard Haskell 2010, defaulting (and the default
statement) only applies to numeric types. GHC (since 6.8.1) supports a language extension ExtendedDefaultRules
that allows defaulting of additional types. You can enable this in .hs files, but it is initially enabled when you start GHCi:
> toEnum 0 ()
where the type has defaulted to (the type) ()
.
You can modify this behaviour by turning off ExtendedDefaultRules
:
> :unset -XExtendedDefaultRules > toEnum 0 Error: Ambiguous type variable...
Mathematical Operations
[edit | edit source]Most mathematical operations in Haskell attempt to mirror "true" mathematical operations. However, none of the operations match perfectly due to overflow, rounding, etc, with different behaviours for different types. In addition, the standard types Double
and Float
represent real numerical values (within some range and only to certain precision), but also include representations of positive and negative infinities and negative zero.
This section compares Haskell mathematical operators and "true" maths.
TODO: Add something on benefit of Infinities, e.g. in f x = 3 - 2 / (4 + 3 / (x - 3))
Arithmetic Operations
[edit | edit source]Addition, Subtraction and Multiplication
[edit | edit source](+)
, (binary) (-)
and (*)
follow the mathematical operations of addition, multiplication and subtraction, subject to overflow and rounding. These operations are defined as methods of the Num
class, and are available for all numeric types. However, they are not closed for all types, for example (-)
is not closed for the Natural
type.
negate
determines the additive inverse, where there is one.
Negative zeros and infinities behave "reasonably", for example:
> negate 0.0 -0.0 > 0.0 == -0.0 True > 5.0 + (-0.0) 5.0 > let inf = fromRational infinity > inf + inf Infinity > inf - inf NaN
From a Haskell syntax perspective, (-)
can be used as a prefix operator to represent negate
(and so can't be used as a section with the operator on the left). There is a function subtract
, where subtract 2 5
means "subtract 2 from 5", which can be used instead.
Division
[edit | edit source]Mathematically, integers are not not closed under division, but other numbers (rational, real, complex) are. Accordingly, Haskell splits division between two classes:
Integral
, which defines methodsdiv
andmod
(and the combineddivMod
) for performing division with remainder (i.e. given integers and , find integers and such that and ).Fractional
, which defines method(/)
for performing division without remainder (i.e. given and , find such that ). It also providesrecip
for finding the multiplicative inverse.
Integral
provides two sets of "division with remainder" functions, illustrated here when dividing -11 by 4:
round | function | function | combined function | example | example |
---|---|---|---|---|---|
towards negative infinity | div |
mod |
divMod |
-3 | 1 |
towards zero | quot |
rem |
quotRem |
-2 | -3 |
In mathematics there is no ability to divide by zero. In Haskell, division by zero is treated differently for different types:
> 5 `div` 0 *** Exception: divide by zero > 5 / 0 Infinity
However, note that these behaviours are not stated requirements of the (/)
or div
class methods. It would be possible for some Integral
types (hand-written, or from other libraries) to return values, or for some Fractional
types to raise exceptions.
Exponentiation
[edit | edit source]Interestingly, Haskell has four different functions corresponding to exponentiation ("raising to a power"):
(^) :: (Integral b, Num a) => a -> b -> a
(^^) :: (Fractional a, Integral b) => a -> b -> a
(**) :: Floating a => a -> a -> a
exp :: Floating a => a -> a
Exercises |
---|
|
scaleFloat
can also be used for floating-point types. scaleFloat n x
gives where r
is the floatRadix
(often 2) of x.
odd
, even
, gcd
and lcm
[edit | edit source]These are all functions that operate on Integral
types, and call methods defined on the Integral
class.
Each integer is either even or odd. even
returns a Bool
indicating whether the argument is even, and odd
returns a Bool
indicating whether the argument is odd.
gcd
calculates the greatest common divisor of two integers, using the Euclidean algorithm. For example, gcd 12 8
= 4.
lcm
uses gcd
to calculate the lowest common multiple. For example, lcm 12 8
= 24.
abs
and signum
[edit | edit source]abs x
determines the absolute value of x, often written . signum x
determines the sign of x.
Both operations are are methods of the Num
class and are available for all numeric types. The two functions adhere to the law that, for any number x, abs x * signum x
. For all standard numeric types, signum
returns a value of "magnitude" 0 or 1, but this is not a general requirement.
For any real number x: abs x
if and abs x
if ; and signum x
is 1, 0 or -1. For types that support negative zero, signum (-0.0)
gives -0.0
.
For a (non-zero) complex number x: signum x
is a complex number with magnitude 1 and the same phase as x; and abs x
is a the magnitude of x (as a complex number with imaginary part of 0). Hence if , then signum x
and abs x
Exercises |
---|
|
Irrational and Transcendental Functions
[edit | edit source]These functions all operate on instances of the Floating
type, on either real or (with the exception of atan2
) complex numbers. There are a number of subtleties around their use:
- precision: these functions produce only approximations to the mathematical results. For example, mathematically , but GHC calculates
sin (-pi)
to be8.742278e-8
[15]. - principle values: mathematically, some "functions", especially "inverse" functions, are multivalued. For example, maps both and to . The inverse function maps to both and . Similarly , which is periodic, maps , and (and infinitely many other values) to . The inverse function maps back to those infinitely many values. Sometimes only a single result is desired and, by convention, the notation usually means only the positive value. Depending on context, may also refer to only a single principle value. The Haskell functions
sqrt
andacos
only return a principle value for any given input:sqrt 4
gives2.0
andacos 1
gives0.0
. These principle values are somewhat (but no universally) standardised. In some cases, for example (the inverse of ), there is only a single result within the real numbers but multiple results (and hence defined principle values) within the complex numbers. - limited domains: mathematically, some functions are undefined for some values, for example is undefined.
Float
orDouble
results are set to the IEEE values ofNaN
,Infinity
or-Infinity
in these cases. For example,log 0
gives-Infinity
[16]. In some cases a function may be undefined for some value when the results are restricted to real numbers, but defined when complex numbers are allowed. For example, there is no within the real numbers, but there is within the complex numbers.sqrt (-1)
givesNaN
, butsqrt ((-1) :+ 0)
gives0.0 :+ 1.0
. In some cases there is a valid real result but none is returned. For example, , but(-8) ** (1/3)
givesNaN
[17]. - discontinuities and branch cuts: some functions are discontinuous at some points. A small change in the input can result is an extreme change in the output. For example is discontinuous (and undefined) at .
tan (pi/2-0.00001)
gives100302.22
buttan (pi/2+0.00001)
gives-99430.34
. When complex numbers are considered, the discontinuities run along lines (often the axes), and are called branch cuts. The locations of these branch cuts depend on the principle values chosen for the functions, as explained below. - negative zeros:
Float
andDouble
also support negative zeros. Most arithmetic functions don't distinguish between0.0
and-0.0
, but some of the irrational and transcendental functions do. For exampleatan2 0 (-1)
gives3.1415927
butatan2 (-0) (-1)
gives-3.1415927
. For complex numbers, branch cuts often lie "between"0.0
and-0.0
. For example,log ((-1) :+ 0)
gives0.0 :+ 3.1415927
andlog ((-1) :+ (-0))
gives0.0 :+ (-3.1415927)
In many cases these subtleties are not problematic, but at times can conspire to create erroneous calculations. In particular, direct implementation of formulae that are mathematically equivalent may not yield computations that give the same results.
Exercises |
---|
|
Principle Values and Branch Cuts
[edit | edit source]The locations of branch cuts depend on the defined principle values.
For example, consider , as illustrated on the diagram on the right. The red line highlights points on a line on the imaginary axis, extending to infinity in both directions. Per Euler's formula, the mapping of points on the red line by all lie on the unit circle, illustrated in blue. The point 0 on the red line maps to 1. As we move up the red line from 0, points map to points anti-clockwise around the circle. maps to and to . The point maps to a point just before and to a point a little after it. As we move down the red line from 0, points map to points further clockwise from 1. maps to and maps to . We can see that both and map to . Similarly and a point at about also map to the same place as each other. In fact, the mapping of the red line wraps around the blue circle infinitely many times, so every point on the blue circle has infinitely many points that map to it.
maps other vertical lines to other circles centred on the origin. Vertical lines to the left of the red line map to smaller circles, and lines to the right map to larger circles. Points on any one vertical line with imaginary parts at and map to the same point as each other, on the negative real axis.
The inverse mathematical function is the complex logarithm. Sometimes it is useful to consider it multi-valued, as illustrated on the left. Each point (except the origin) will map to points vertically aligned in different layers in the diagram. maps to and to (and to infinitely many other points). As travels around a circle, its mappings move around the surface following a helix, and moving up a whole level for each full circuit of the circle. There are no branch cuts.
Sometimes it's more convenient for its range to be restricted so that it has single, principle value. Typically (but not universally), this range is defined as those numbers with imaginary part in the range . Then maps to only the principle value . The range of for all the points on the blue circle is illustrated by the green line (excluding the point ), which represents a subset of the points on the original red line. There is a branch cut along the negative real axis: points on it (or just above it) map to points with an imaginary part of (or just less than) . Points just below it map to points with an imaginary part just greater than .
The Haskell function exp
mirrors the behaviour of , except for rounding errors. log
(for Float
and Double
) behaves the same as the restricted, single-valued, function , but with a slightly different range due to the presence of negative zeros. It maps points with imaginary part 0.0
to points with imaginary part and points with imaginary part -0.0
to points with imaginary part . The branch cut is "between" 0.0
and -0.0
on the imaginary axis:
> log $ (-1) :+ 0 0.0 :+ 3.1415927 > log $ (-1) :+ (-0) 0.0 :+ (-3.1415927)
The range of log
is those numbers with imaginary part in the range . It is represented by the green line, including the point .
Both (under ) and (-1) :+ 0
(under log
) map to , indicated by the filled-in arrow on the diagram. Only (-1) :+ (-0)
maps (under log
) to , indicated by the unfilled arrow.
One consequence of the branch cut is that log
is only the inverse of exp
for values within the range of log
:
> log $ exp $ 0 :+ 3 0.0 :+ 3.0 > log $ exp $ 0 :+ 4 0.0 :+ (-2.2831855)
There is not a single agreed definition for either the principle values or branch cuts, and they may be different for different types, in other languages, in other systems and in different mathematical usage. It would, for example, be possible for a different (e.g. user-defined) type to define the range of log
to be those numbers with imaginary part in the range , with a branch cut on the negative imaginary axis.
Rounding and Branch Cuts
[edit | edit source]Sometimes, rounding errors result in values being the "wrong side" of branch cuts. For example:
> log $ exp $ 0 :+ pi :: Complex Float 0.0 :+ (-3.1415927)
0 :+ pi
is within the range of log
, so we would expect the result to be 0 :+ 3.1415927
. The problem is clear if we look at the intermediate result:
> exp $ 0 :+ pi :: Complex Float (-1.0) :+ (-8.742278e-8)
We can see the value has "overshot" by a little bit, taking it to the wrong side of the branch cut. (Mathematically, . Ideally, exp $ 0 :+ pi
would give (-1.0) :+ 0.0
, and exp $ 0 :+ (-pi)
would give (-1.0) :+ (-0.0)
).
In this case, when we use Double
, which operates to greater precision, it behaves as expected:
> log $ exp $ 0 :+ pi :: Complex Double 0.0 :+ 3.141592653589793
Unfortunately this problem is sometimes hard to detect and code around. It is also not limited to problems with the inaccuracy of the algorithms for the core functions such as exp
. For example, consider f x = 1 - (x+1)/x
. Mathematically, this should be negative when x > 0, and tend to zero as x increases. To start with, the function behaves as expected:
> f 10000 -1.00016594e-4 > f 100000 -1.001358e-5
However, there comes a point when the precision of Float
means it can't distinguish x+1
and x
. At this point, the function stops returning a negative value:
> f 16777214 -1.1920929e-7 > f 16777215 -1.1920929e-7 > f 16777216 0.0
This by itself may not be a big problem. But now consider g x = signum (f x)
. When x > 0, g x
should return -1.0
. However, as soon as x reaches the "failure point", it suddenly jumps to 0.0
, giving an unexpected discontinuity and an inaccurate result:
> g 16777214 -1.0 > g 16777215 -1.0 > g 16777216 0.0
A similar problem can occur with branch cuts, e.g. with h x = atan2 (f x) (-1)
:
> h 16777214 -3.1415925 > h 16777215 -3.1415925 > h 16777216 3.1415927
Or with some build-in functions[18]:
> imagPart $ atanh $ (-16777217) :+ (-3) -1.5707964 > imagPart $ atanh $ (-16777218) :+ (-3) -1.5707964 > imagPart $ atanh $ (-16777219) :+ (-3) 1.5707964
Underflow and Denormalization
[edit | edit source]Exception Handling
[edit | edit source]Note
Throughout this page I have tried to use the words "exception" and "error" as described here.
Some numeric operations raise exceptions in some situations, such as division by zero. It would generally be an error to allow these to occur without handling the exceptions[19]. The following sections are some of the ways to deal with this.
An Aside on Avoiding Errors using Types
[edit | edit source]It is often best to design data types so that it is impossible to write erroneous programs. As a contrived example, compare the following:
data Suit = Hearts | Diamonds | Clubs | Spades deriving Show
data SuitColour = Red | Black deriving Show
suitColour :: Suit -> SuitColour
suitColour Hearts = Red
suitColour Diamonds = Red
suitColour Clubs = Black
suitColour Spades = Black
with:
--Note:
--0 = Hearts, 1 = Diamonds, 2 = Clubs, 3 = Spades
--0 = Black, 1 = Red
suitColour :: Int -> Int
suitColour 0 = 0
suitColour 1 = 0
suitColour 2 = 1
suitColour 3 = 1
The second version would allow suitColour 152
to compile, which would give an error at run-time. The first version ensures we can't write such erroneous code (as well as being a lot easier to read!).
Suppose we tried to adopt this approach to avoid division by zero errors. We could define a NonZeroInteger
type and redefine div
with the type signature div :: Integer -> NonZeroInteger -> Integer
[20]. Now we have to fix errors like x `div` 0
at compile time and would never get a division by zero run-time exception.
But at some point we might want to code e.g. x `div` (y - z)
. And now we have a problem:
- we may want to allow either y or z to individually be zero (hence they would not be of type
NonZeroInteger
, so we'd need to convert them toNonZeroInteger
in order to pass the result todiv
); and - y and z might be the same, so
(x - y)
would be zero and the conversion would fail with a run-time exception that zero is out-of-bounds forNonZeroInteger
.
This attempt to use types to catch division by zero at compile time has failed: we've just swapped one possible run-time exception for another. (And added complexity to the types).
Program logic
[edit | edit source]One way to avoid run-time exceptions is to include logic in your program, especially at places where you get data from the outside world (e.g. a user).
Here's an example program:
doDiv :: IO ()
doDiv = do
putStr "Enter an integer: "
x <- readLn
let (d,r) = 100 `divMod` x
putStrLn $ "100 divided by " ++ show x ++ " is " ++ show d ++ " remainder " ++ show r
It has a couple of problems, including that the user could type non-integer values (which we'll ignore for now), and that they or they could type in "0".
Exercises |
---|
|
"Safe" functions
[edit | edit source]One approach often advocated for pure functions (i.e. where the result depends only on the arguments passed to it) is have the function return Nothing
for those values where it "fails". For example, the function:
stripPrefix :: Eq a => [a] -> [a] -> Maybe [a]
drops the given prefix from a list, but returns Nothing
if the list did not start with it. There are also a number of safe functions in other modules (such as the cunningly named Safe
, that provides safe version of list functions such as head
).
Similarly, we could write "safe" versions of div
etc:
safeDiv :: Integral a => a -> a -> Maybe a
_ `safeDiv` 0 = Nothing
x `safeDiv` y = Just $ x `div` y
Although a reasonable approach, it might be quite a challenge to build "safe" versions for all of the functions you use, especially since you would have to check the error conditions for each of them.
Exercises |
---|
|
Note that you could use Either
instead of Maybe
to similar effect, and this would allow the response to indicate the type of error that occurred (not just that there was one).
Catching Exceptions
[edit | edit source]Another way to handle exceptions is with catch
. This is defined as:
catch :: Exception e => IO a -> (e -> IO a) -> IO a
Both the result of catch
and its first argument are IO
monads. The second argument is an "exception handler". When executed, catch
executes its first argument and, if an exception is raised, the "handler" is executed with the value of the exception passed as an argument. Otherwise, the result is returned as normal.
This works well with many IO
actions, such as opening a file:
catch (readFile f)
(\e -> do let err = show (e :: IOException)
hPutStr stderr ("Warning: Couldn't open " ++ f ++ ": " ++ err)
return "")
Here's a first attempt at using it in doDiv
:
doDiv = do
putStr "Enter an integer: "
x <- readLn :: IO Integer
catch (do let (d,r) = 100 `divMod` x
putStrLn $ "100 divided by " ++ show x ++ " is " ++ show d ++ " remainder " ++ show r)
(\e -> putStrLn $ const "Sorry, I can't divide by zero." (e :: ArithException))
Which gives the following:
> doDiv Enter an integer: 0 100 divided by 0 is Sorry, I can't divide by zero.
which isn't quite what we want. The problem is that, as a result of lazy evaluation, putStrLn
is happy to start displaying the result of "100 divided by ...
before attempting to evaluate the div
. The div
is only evaluated when putStrLn
needs the (++)
to evaluate show d
, and this is when the exception is triggered. The behaviour would be the same if the let (d,r)
is outside of the catch
.
However, we can "force" the evaluation of the div
earlier, using evaluate
:
evaluate :: a -> IO a
evaluate $ 100 `divMod` x
will create an IO
action which, when executed, will force evaluation of the div
, and throw the exception if appropriate.
Exercises |
---|
|
Note that evaluate
doesn't "fully evaluate" expressions. For example, it would not cause an exception in head <$> evaluate [3, 4 `div` 0]
[21].
Catching Exceptions can be Painful
[edit | edit source]It might be nice if we could display tables showing how numeric operators worked, like this:
> putOpTable (-) [0, 1, 2 :: Int] 0 1 2 0 0 -1 -2 1 1 0 -1 2 2 1 0
But we need to be careful with operations that throw exceptions, so that we get this:
> putOpTable div [0, 1, 2 :: Int] 0 1 2 0 - 0 0 1 - 1 0 2 - 2 1
Our first attempt at putOpTable
might start like this:
putOpTable :: Show a => (a -> a -> a) -> [a] -> IO ()
putOpTable op xs = do
putStrLn titleLine
mapM_ (putStrLn . opLine) xs
Exercises |
---|
|
You probably found the conversion to putOpTableCatch
quite painful. We can no longer construct a string for each line using a pure function, since we want to catch exceptions in the middle of the string. Instead we have to execute putStr
for each cell in the table, handing exceptions as we go, and much more of the code is written in the IO
monad.
Catching Exceptions in Pure Code
[edit | edit source]Wouldn't it be nice if we could catch exceptions in pure code? Then we could keep the pure function opLine
in putOpTable
above, replacing any exceptions with the "-" character we want.
There is a way to do this, using this function with a slightly scary name from the module System.IO.Unsafe
:
unsafePerformIO :: IO a -> a
which allows an IO computation to be performed at any time[22]. It comes with a warning: For this to be safe, the IO computation should be free of side effects and independent of its environment. However, we only want to evaluate (pure) expressions and handle the exceptions they throw, so I think we're OK[23].
We can define:
checkArith :: a -> Maybe a
checkArith x = unsafePerformIO $
catch (Just <$> evaluate x)
(\e -> return $ const Nothing $ (e :: ArithException))
Now:
> checkArith $ 100 `div` 3 Just 33 > checkArith $ 100 `div` 0 Nothing > putOpTableSafe (\x y -> checkArith $ x `div` y) [0, 1, 2] 0 1 2 0 - 0 0 1 - 1 0 2 - 2 1
Exercises |
---|
|
Example Num Instances
[edit | edit source]Mod3
[edit | edit source]Let's create a new numeric type that does arithmetic modulo 3, so that addition works like this:
+ | 0 | 1 | 2 |
---|---|---|---|
0 | 0 | 1 | 2 |
1 | 1 | 2 | 0 |
2 | 2 | 0 | 1 |
Here's a start:
data Mod3 = Zero | One | Two
deriving (Eq, Ord, Enum, Bounded, Show, Read)
Note that we can automatically derive instances of many standard classes for this type. Also, this shows that being a "numeric" doesn't necessarily mean we have to use numeric digits (0, 1, 2) to represent the values.
Let's make this an instance of the appropriate numeric classes.
Num
Instance
[edit | edit source]In order to do addition using (+)
, we need to make this an instance of Num
:
instance Num Mod3 where
Zero + Zero = Zero
Zero + One = One
...
OK - I got bored, so your turn:
Exercises |
---|
|
Real
and Integral
Instances
[edit | edit source]Exercises |
---|
|
Show
and Read
Instances
[edit | edit source]Exercises |
---|
|
Word8T
[edit | edit source]The standard Word8
type performs arithmetic modulo 256. Sometimes it would be useful to have a type that throws exceptions on overflow instead, so let's create type Word8T
that does that.
Exercises |
---|
|
Notes
- ↑ means the set of natural numbers including zero. (The set of natural numbers starting from 1 is represented by ).
- ↑ a b Hence may be different in different implementations. The Haskell specification requires
Int
to have a range of at least[-2^29 .. 2^29-1]
, andWord
will use the same number of bits. - ↑
Rational
is simply atype
synonym forRatio Integer
- ↑ You can also create a value of type
Complex Bool
, for example(True :+ False :: Complex Bool)
. However, this type is not an instance ofNum
, so you can't do addition or other maths with it. - ↑ Polynomials aren't actually numbers, but they can be considered "numeric" since you can do many mathematical operations with them, like addition, multiplication, etc.
- ↑ Although it is often written in infix notation, e.g.
3 + 4
. - ↑ internally, these implementation do use low-level, inaccessible, functions such as
integerToFloat#
,integerToDouble#
, etc. - ↑ This behaviour is a bit different to that in many object oriented languages, where a declaration similar to
fromInteger :: Num a => Integer -> a
might mean thatfromInteger
can return any type it likes, as long as it is an instance ofNum
. - ↑ it is also possible to determine the type by default.
- ↑ In the actual code, the fixity declarations need to be adjacent to the function declarations.
- ↑ and specifically
4^3^2
is the same as4^(3^2)
. - ↑ Unary
-
always means thenegate
function defined in Prelude, and always with precedence 6. This is true even if the binary operator-
and/ornegate
have been rebound in a local module. - ↑ Ignore the fact that the type variable has weirdly renamed.
Num p => p
andNum a => a
are identical. - ↑ it is also possible to fix using the Haskell
TypeApplications
extension. - ↑ These results are all given using
default (Float)
. - ↑ It is possible to one of these values in just one part of a complex number, for example
log (0 :+ 0)
gives(-Infinity) :+ 0.0
. - ↑ This is in part due to the implementation of
x ** y
(which useslog x
as an intermediate calculation). Happily, it helps ensure that the result doesn't conflict with the principle value when complex results are allowed:((-8) :+ 0) ** (1/3)
gives1.0 :+ 1.7320509
. - ↑ The definition in
Data.Complex
isatanh z = 0.5 * log ((1.0+z) / (1.0-z))
. A "mathematically equivalent" definitionatanh z = 0.5 * (log (1.0+z) - log (1.0-z))
would not have this deficiency. - ↑ The standard floating-point types
Float
andDouble
return valuesInfinity
orNaN
instead of raising exceptions for division by zero. It would also be an error to allow these to occur without correctly handling the results. - ↑ We would presumably define a class
NonZeroIntegral
and try to make this polymorphic. - ↑ Technically, it evaluates to weak head normal form.
- ↑ Some interesting background on this function, in particular for creating functions that "remember" previous results for certain parameter values so that it doesn't need to repeat expensive computations, is here.
- ↑ It probably wouldn't be OK to
readFile
withinusafePerformIO
, for example, since (amongst other issue), the relative order in which the side effect take place (relative to the main I/O trunk, or other calls to unsafePerformIO) is indeterminate.