|
|
> [Note: When a type has only one value constructor function, it is
> customary for it to have the same name as the type. In other words,
>
> data Complex = Complex Double Double
>
> I have deliberately not done this, in the interests of clarity.]
It's perhaps not clear what I'm talking about here. Consider, for example
data Complex = Grid Double Double | Polar Double Double
Now I can write a complex number in *two* ways:
Grid 5 7
which represents 5 + 7i, or
Polar 3 pi
which represents -3 (i.e., magnitude=3, argument=pi radians). I can then
write
mulCx (Grid r1 i1) (Grid r2 i2) = Grid (r1*r2 - i1*i2) (r1*i2 + r2*i1)
mulCx (Polar m1 a1) (Polar m2 a2) = Polar (m1*m2) (a1+a2)
Unfortunately, if one number is Grid and the other is Polar, this
function will throw a "pattern match failure" exception, because no
pattern matches that exact combination. I'm not saying this approach is
a good idea, I'm offering it as an example of what you *could* do.
Now, "Grid" and "Polar" are value constructors; they are ways of
constructing (and de-constructing) data of the Complex type. But our
original definition had only one constructor. Why have two names for one
thing? This is why people typically write
data Complex = Complex Double Double
I didn't, though, to make it clear when I'm talking about a type and
when I'm talking about a constructor.
> In C-based languages, if-statements are a language primitive, whereas
> switch/case blocks are just syntax sugar for making nested if-blocks
> easier to write. In Haskell, the reverse is true; *pattern matching* is
> the fundamental language primitive behind all conditional branching. An
> if-expression is simply syntax sugar for a pattern match where the
> patterns are "True" and "False". Likewise, the "==" function is
> implemented in terms of pattern matching, NOT THE OTHER WAY AROUND! (You
> can pattern-match on stuff for which the "==" function isn't defined!)
The general way to do pattern-matching is with a case-block:
case z of
Grid r i -> ...stuff...
Polar m a -> ...stuff...
An if-expression is merely syntax sugar:
if foo then bar else baz
case foo of
True -> bar
False -> baz
Writing multiple equations for a single function is also syntax sugar:
isReal (Cx r 0) = True
isReal _ = False
isReal z =
case z of
Cx r 0 -> True
_ -> False
> So much for putting stuff in and getting stuff out; how about some
> actual *arithmetic* now?
>
> > addCx :: Complex -> Complex -> Complex
> > addCx (Cx r1 i1) (Cx r2 i2) = Cx (r1 + r2) (i1 + i2)
>
> That type signature looks a bit weird, eh? The basic rule for reading
> function signatures is this: The *last* type is the return type of the
> function. All the *other* types are argument types. So if you see 5
> things separated by arrows, than you have a 4-argument function, and the
> 5th type is what the function returns.
If you want to make your head explode:
addCx :: Complex -> (Complex -> Complex)
Similarly,
quadratic :: Complex -> (Complex -> Complex)
This is why "quadratic c" is a valid expression; the result is a
function of type Complex -> Complex.
> If you do compile this post, you may get several warnings about
> "undefined methods". The Num class actually defines more than just the
> three methods above. We've written an implementation for Num without
> implementing *all* of the prescribed methods. Haskell is usually very
> strict about correctness and compile-time checks. But not implementing
> all the methods of a class is a *run-time* error. (Or rather, trying to
> *use* one of the unimplemented methods is a run-time error. Since we
> aren't going to use any of the unimplemented methods, we'll be fine.)
Haskell's number classes are notoriously broken. They were defined very
early on in Haskell's development. (Numbers are one of the very first
constructs you're going to need out of a programming language!)
Naturally, to change them now would massively break every shred of
Haskell code ever written. On top of that, nobody can decide on a *less*
broken alternative.
For the curious, the *actual* definition of Num is
class Num n where
(+) :: n -> n -> n
(-) :: n -> n -> n
(*) :: n -> n -> n
negate :: n -> n
abs :: n -> n
signum :: n -> n
fromInteger :: n -> n
You may notice that abs is a rather stupid function for a complex number
to have; what do you do? Take the absolute value of the real and
imaginary parts separately? (Note that what you *can't* do is have abs
return a real-value as the answer; it must be another complex value.)
Ditto for signum, which usually returns -1, 0 or +1 for normal types.
Finally, fromInteger converts an integer into a Complex (or whatever).
Note that I said earlier that "sum" now works for Complex. I was wrong;
without fromInteger, you can't construct the complex number 0, which is
what "sum" starts adding up from. Similarly, "product" wants to do
"fromInteger 1" for its start value.
Things rapidly get vastly more broken if you want to handle *fractional*
values. Notice (*) is here, but (/) is conspicuously missing? That's
because integer types have "div", which does integer division, while
fractional types have "/", which does normal division. (Integer types
also have "mod", while fractional types do not.) Trigonometric functions
are even more fun!
> Knowing this, let us define the following function:
>
> > orbit :: Complex -> [Complex]
> > orbit c = iterate (quadratic c) (Cx 0 0)
Now you see why it's Cx 0 0, rather than just 0. If you add
> fromInteger r = Cx (fromInteger r) 0
to the instance declaration, then these problems go away. (And "sum" and
"product" will start working properly.) Notice that "r" is an Integer,
so we need to recursively call "fromInteger" to convert it to a Double.
> My, that's a lot of brackets! Fortunately, Haskell has something a bit
> like shell piping:
>
> count c = length $ take 15 $ takeWhile (\ z -> mag2 z <= 4) $ orbit c
This is not actually part of the language specification. Actually, it's
just a user-defined operator with *really* low precedence:
> ($) :: (x -> y) -> x -> y
> f $ x = f x
>
> infixr 0 $
That *looks* like a really pointless function, but it's utility is in
the operator precedence.
> We could also have done it this way:
>
> main = putStrLn (unlines output)
You can also do this:
> main = writeFile "Mandelbrot.txt" (unlines output)
This then saves the data to a text file (in the system default encoding,
with the system default end-of-line convention), rather than printing it
to stdout.
Post a reply to this message
|
|