POV-Ray : Newsgroups : povray.off-topic : How does Haskell work? : How does Haskell work? Server Time
4 Sep 2024 09:16:28 EDT (-0400)
  How does Haskell work?  
From: Invisible
Date: 17 Mar 2010 09:52:11
Message: <4ba0de8b$1@news.povray.org>
Under the exotic syntax, what's really going on with Haskell?



Let's start by taking a short look at how an OO language works.

In an ideal OO language, conceptually every item of data is an object, 
and every operation on that data is a method call. For, conceptually, 
when you say "2 + 3", what happens is that a "2" object is created, a 
"3" object is created, and then the "+" method is invoked.

Now in reality, if you really are created heap-allocated objects and 
doing dynamic method lookup every time you perform even the most trivial 
arithmetic, the system is going to eat RAM like candy and run slower 
than molasses. So every language that does this has something happening 
under the covers to take away the overhead for certain hard-wired kinds 
of objects. But still, *in principle* everything is objects and methods.

So, ignoring the machinery for defining classes for a moment, code 
written in an OO language can create variables, read/write them, take 
conditional branches (if/then/else, for, while, etc.) and invoke 
methods. (Object creation can be regarded as a special case of a method 
call. Indeed, in some languages it *is* a method call.)

Obviously I'm glossing over a whole heap of important details about OOP. 
But then, everybody already knows all about OOP, so I don't think I need 
to repeat that detail.



So, what is Haskell's execution model then?

Haskell as Algebraic Data Types (ADTs - not to be confused with 
_Abstract_ Data Types, which are something completely different). An 
object is something like a C "struct" or a Pascal "record", but an ADT 
is more like what C calls a "union".

An ADT consists of one or more named "constructors", and each such 
constructor has zero or more "fields" of programmer-specified type. The 
fields can optionally be given names.

At one end of the spectrum, you can define "record types". For example, 
you might define a "date" type that has a single constructor, with 
fields named "day", "month" and "year". Nothing especially unusual there.

At the other end of the spectrum, you can define "enumeration types". 
That is, types with multiple nullary constructors. (A nullery 
constructor meaning a constructor with zero fields.) An obvious example 
is "bool", with its "true" and "false" constructors. A less obvious 
example might be "AccessMode", with "ReadMode", "WriteMode" and 
"AppendMode" as constructors.

Other languages have records. A few other languages have true 
enumerations. But Haskell has ADTs, which are a weird hybrid of the two. 
The only other language I can think of that approaches this is C with 
its "union" construct.

Put simply, you can have multiple constructors each with multiple 
fields. What does this actually mean? It means that a data structure of 
a single type can have more than one possible structure. For example, 
you could make a "point" type that can hold both 2D and 3D types. Or a 
"colour" type that can hold both RGB and CYMK colours.

The thing is, usually you would *want* these to be seperate types. So 
this is really a poor example.

For a better example, suppose for argument's sake that we want a binary 
tree that holds data in its leaves only, and no other auxilery 
information. You can do this by defining a "tree" type with two 
constructors: "leaf", which has a single field for the data item, and 
"branch", which has two fields, one for each subtree.

This is the intended use case for ADTs. Every tree node is either a leaf 
or a branch. Every function that processes trees needs to handle both. 
If you replace a simple binary tree with a complex parse tree, with 
different node types for each possible kind of statement or 
subexpression, you start to see the utility and power of ADTs.

Notionally, *everything* is an ADT. The integer type is just an 
enumeration type that has 2^32 constructors, which just happen to be 
named "1", "2", "3"... Similar remarks apply to the character type. 
Obviously these types are "special" to the compiler, but to the casual 
Haskell programmer, there is no *visible* difference. They are just 
normal ADTs with a special syntax.



ADTs dovetail with a Haskell feature called "pattern matching". Haskell 
actually allows sophisticated multi-level patterns, but these can be 
easily translated into nested single-level pattern matches. (And, 
indeed, the compiler performs this translation.)

A single-level pattern match takes a value, inspects it, and does a 
multi-way branch based on which constructor it finds. For example, a 
function working on a tree would execute one code path for a leaf node, 
and another code path for a branch node. The pattern can also can 
variables to the fields of a constructor; this is in fact the only way 
to access those fields.

Pattern matching is in fact the only conditional execution primitive in 
Haskell. (For example, the if/then/else expression is really just syntax 
sugar for pattern matching against "true" and "false".)

In addition, Haskell is a "lazy" language. Data is not computed unless 
or until it is actually "needed". Pattern matching is the thing that 
"needs" results to be computed. You can't decide which execution path to 
take if the data has not been computed yet. So pattern matching is the 
thing that actually makes computations run. (The other thing is I/O 
operations, since you can't output something that hasn't been computed yet.)

A common mistake is to think of data as either "evaluated" or 
"unevaluated". While something like an integer either exists or it 
doesn't, more complex compound data structures can exist partially. For 
example, the list [1,2,3] consists of 7 data objects - three list nodes, 
a list end marker, and three integers. A list node cannot exist before 
the node ahead of it exists, but beyond that, any combination of these 7 
objects may or may not have been computed at any moment in time.

A single-level pattern match only computes the top level of a value. 
Technically, it reduces a value to Weak Head-Normal Form (WHNF). That 
is, it evaluates it just enough to find out which constructor is 
present. Any fields the constructor has remain unevaluated. (Unless 
there are strictness annotations, but forget about that for now.)

For completeness, when something has been *totally* evaluated, this is 
Normal Form (NF).

So, to recap, a single-level pattern match performs the following actions:

1. Evaluate a value to WHNF.

2. Perform a multi-way conditional jump based on the constructor found.

3. Optionally bind variables to the fields of the constructor.

This is the only construct for accessing the fields of a data structure 
(even if there is only a single constructor for that type), and one of 
the only ways to make stuff actually evaluate. (Notionally, you could 
claim that I/O evaluates stuff because it returns data to the Runtime 
System which then pattern matches against it.)

Pattern matching is used for directly manipulating data structures. 
Typically you define a data type, use pattern matching to define the 
functions that operate on it in the most primitive way, and then define 
complex functions in terms of the simpler ones. Indeed, it is common to 
define a type, and export its name but *not* its internal structure. 
This allows encapsulation in a mannar similar to OOP. (Not inheritance 
or dynamic binding, just encapsulation.)



Now we're starting to get a feel for how Haskell works. Every value 
notionally consists of a "tag" (the constructor) which indicates what 
fields will follow (if any). Pattern matching forces expressions to 
execute, and does a multi-way conditional jump based on the tag found in 
the result, binding local variables to the fields so they can be accessed.

In fact, the entire Haskell language gets compiled down to just 6 
primitive operations:

1. Create a data literal.

2. Create an anonymous function.

3. Call a function with some arguments.

4. Create a variable and bind it to an unevaluated expression.

5. Read a value from a variable.

6. Pattern match.

That's the entire Haskell language, right there. Everything that Haskell 
can do is represented here. This is the actual internal representation 
that GHC uses when compiling Haskell code. In particular:

- Primitive #4 is the *only* place where memory is allocated.

- Primitive #6 is the *only* place where unevaluated expressions get 
evaluated. It is also the only place where conditional branching happens.

Bear in mind that GHC's internal code representation has different 
properties to the original Haskell source code. (For example, creating a 
variable in Haskell doesn't _necessarily_ mean that any memory will be 
allocated. The compiler might optimise the variable away.)


Post a reply to this message

Copyright 2003-2023 Persistence of Vision Raytracer Pty. Ltd.