Understanding Computation (8 page)

Read Understanding Computation Online

Authors: Tom Stuart

Tags: #COMPUTERS / Programming / General

BOOK: Understanding Computation
4.17Mb size Format: txt, pdf, ePub
Applications

Our earlier
implementation of small-step semantics makes only
moderate use of the Ruby call stack: when we call
#reduce
on a large program, that might cause
a handful of nested
#reduce
calls
as the message travels down the abstract
syntax tree until it reaches the piece of code that is
ready to reduce.
[
16
]
But the virtual machine does the work of tracking the
overall progress of the computation by maintaining the current program
and environment as it repeatedly performs small reductions; in
particular, the depth of the call stack is limited by the depth of the
program’s syntax tree, since the nested calls are only being used to
traverse the tree looking for what to reduce next, not to perform the
reduction itself.

By contrast, this big-step implementation makes much greater use
of the stack, relying entirely on it to remember where we are in the
overall computation, to perform smaller computations as part of
performing larger ones, and to keep track of how much evaluation is
left to do. What looks like a single call to
#evaluate
actually turns into a series of
recursive calls, each one evaluating a subprogram deeper within the
syntax tree.

This difference highlights the purpose of each approach. Small-step semantics assumes
a simple abstract machine that can perform small operations, and therefore includes
explicit detail about how to produce useful intermediate results; big-step semantics
places the burden of assembling the whole computation on the machine or person executing
it, requiring her to keep track of many intermediate subgoals as she turns the entire
program into a final result in a single operation. Depending on what we wish to do with a
language’s operational semantics—perhaps build an efficient implementation, prove some
properties of programs, or devise some optimizing transformations—one approach or the
other might be more appropriate.

The most influential use of
big-step semantics for specifying real programming languages is Chapter 6 of
the
original
definition of the Standard ML programming language
, which explains all of the
runtime behavior of ML in big-step style. Following this example, OCaml’s core
language has a
big-step
semantics
to complement its more detailed small-step definition.

Big-step operational semantics
is also used by the W3C: the
XQuery 1.0 and XPath 2.0
specification
uses mathematical inference rules to describe
how its languages should be evaluated, and the
XQuery and XPath Full
Text 3.0 spec
includes a big-step semantics written in
XQuery.

It probably hasn’t escaped your attention that, by writing down
Simple
’s small- and big-step semantics in Ruby instead of
mathematics, we have implemented two different Ruby
interpreters
for it. And this is what operational semantics really is: explaining the
meaning of a language by describing an interpreter. Normally, that description would be
written in simple mathematical notation, which makes everything very clear and unambiguous
as long as we can understand it, but comes at the price of being quite abstract and
distanced from the reality of computers. Using Ruby has the disadvantage of introducing
the extra complexity of a real-world programming language (classes, objects, method
calls…) into what’s supposed to be a simplifying explanation, but if we already understand
Ruby, then it’s probably easier to see what’s going on, and being able to execute the
description as an interpreter is a nice
bonus.

Denotational Semantics

So far, we’ve looked at the
meaning of programming languages from an operational perspective, explaining what
a program means by showing what will happen when it’s executed. Another approach,
denotational semantics
, is concerned instead with translating
programs from their native language into some other representation.

This style of semantics doesn’t directly address the question of executing a program at
all. Instead, it concerns itself with leveraging the established meaning of another
language—one that is lower-level, more formal, or at least better understood than the language
being described—in order to explain a new one.

Denotational semantics is necessarily a more abstract approach than operational, because
it just replaces one language with another instead of turning a language into real behavior.
For example, if we needed to explain the meaning of the English verb “walk” to a person with
whom we had no spoken language in common, we could communicate it operationally by actually
walking back and forth. On the other hand, if we needed to explain “walk” to a French speaker,
we could do so denotationally just by telling them the French verb

marcher
”—an undeniably higher level form of communication,
no messy exercise required.

Unsurprisingly, denotational semantics is conventionally used to
turn programs into mathematical objects so they can be studied and
manipulated with mathematical tools, but we can get some of the flavor of
this approach by looking at how to denote
Simple
programs in some other way.

Let’s try giving a denotational semantics for
Simple
by
translating it into Ruby.
[
17
]
In practice, this means turning an abstract syntax tree into a string of Ruby code
that somehow captures the intended meaning of that syntax.

But what is the “intended meaning”? What should Ruby denotations of our expressions and
statements look like? We’ve already seen operationally that an expression takes an environment
and turns it into a value; one way to express this in Ruby is with a proc that takes some
argument representing an environment argument and returns some Ruby object representing a
value. For simple constant expressions like «
5
» and
«
false
», we won’t be using the environment at all, so we
only need to worry about how their eventual result can be represented as a Ruby object.
Fortunately, Ruby already has objects specifically designed to represent these values: we can
use the Ruby value
5
as the result of the
Simple
expression «
5
», and
likewise, the Ruby value
false
as the result of «
false
».

Expressions

We can use this
idea to write implementations of a
#to_ruby
method for the
Number
and
Boolean
classes:

class
Number
def
to_ruby
"-> e {
#{
value
.
inspect
}
}"
end
end
class
Boolean
def
to_ruby
"-> e {
#{
value
.
inspect
}
}"
end
end

Here is how they behave on the console:

>>
Number
.
new
(
5
)
.
to_ruby
=> "-> e { 5 }"
>>
Boolean
.
new
(
false
)
.
to_ruby
=> "-> e { false }"

Each of these methods produces a string that happens to contain
Ruby code, and because Ruby is a language whose meaning we already
understand, we can see that both of these strings are programs that
build procs. Each proc takes an environment argument called
e
, completely ignores it, and returns a Ruby
value.

Because these denotations are strings of Ruby source code, we can
check their behavior in IRB by using
Kernel#eval
to turn
them into real, callable
Proc
objects:
[
18
]

>>
proc
=
eval
(
Number
.
new
(
5
)
.
to_ruby
)
=> #
>>
proc
.
call
({})
=> 5
>>
proc
=
eval
(
Boolean
.
new
(
false
)
.
to_ruby
)
=> #
>>
proc
.
call
({})
=> false
Warning

At this stage, it’s tempting to avoid procs entirely and use simpler implementations
of
#to_ruby
that just turn
Number.new(5)
into the string
'5'
instead
of
'-> e { 5 }'
and so on, but part of the point of
building a denotational semantics is to capture the essence of constructs from the source
language, and in this case, we’re capturing the idea that expressions
in
general
require an environment, even though these specific expressions don’t
make use of it.

To denote expressions that do use the environment, we need to decide how environments
are going to be represented in Ruby. We’ve already seen environments in our operational
semantics, and since they were implemented in Ruby, we can just reuse our earlier idea of
representing an environment as a hash. The details will need to change, though, so beware
the subtle difference: in our operational semantics, the environment lived inside the
virtual machine and associated variable names with
Simple
abstract syntax trees like
Number.new(5)
, but in our
denotational semantics, the environment exists in the language we’re translating our
programs into, so it needs to make sense in that world instead of the “outside world” of a
virtual machine.

In particular, this means that our denotational environments should associate variable
names with native Ruby values like
5
rather than with
objects representing
Simple
syntax. We can think of an
operational environment like
{ x: Number.new(5) }
as
having a denotation of
'{ x: 5 }'
in the language we’re
translating into, and we just need to keep our heads straight because both the
implementation metalanguage and the denotation language happen to be Ruby.

Now we know that the environment will be a hash, we can implement
Variable#to_ruby
:

class
Variable
def
to_ruby
"-> e { e[
#{
name
.
inspect
}
] }"
end
end

This translates a variable expression into the source code of a
Ruby proc that looks up the appropriate value in the environment
hash:

>>
expression
=
Variable
.
new
(
:x
)
=> «x»
>>
expression
.
to_ruby
=> "-> e { e[:x] }"
>>
proc
=
eval
(
expression
.
to_ruby
)
=> #
>>
proc
.
call
({
x
:
7
})
=> 7

An important aspect of denotational semantics is that it’s
compositional
: the denotation of a program is
constructed from the denotations of its parts. We can see this
compositionality in practice when we move onto denoting larger
expressions like
Add
,
Multiply
, and
LessThan
:

class
Add
def
to_ruby
"-> e { (
#{
left
.
to_ruby
}
).call(e) + (
#{
right
.
to_ruby
}
).call(e) }"
end
end
class
Multiply
def
to_ruby
"-> e { (
#{
left
.
to_ruby
}
).call(e) * (
#{
right
.
to_ruby
}
).call(e) }"
end
end
class
LessThan
def
to_ruby
"-> e { (
#{
left
.
to_ruby
}
).call(e) < (
#{
right
.
to_ruby
}
).call(e) }"
end
end

Here we’re using string concatenation
to compose the denotation of an expression out of the
denotations of its subexpressions. We know that each subexpression will
be denoted by a proc’s Ruby source, so we can use them as part of a
larger piece of Ruby source that calls those procs with the supplied
environment and does some computation with their return values. Here’s
what the resulting denotations look like:

>>
Add
.
new
(
Variable
.
new
(
:x
),
Number
.
new
(
1
))
.
to_ruby
=> "-> e { (-> e { e[:x] }).call(e) + (-> e { 1 }).call(e) }"
>>
LessThan
.
new
(
Add
.
new
(
Variable
.
new
(
:x
),
Number
.
new
(
1
)),
Number
.
new
(
3
))
.
to_ruby
=> "-> e { (-> e { (-> e { e[:x] }).call(e) + (-> e { 1 }).call(e) }).call(e) <
(-> e { 3 }).call(e) }"

These denotations are now complicated enough that it’s difficult
to see whether they do the right thing. Let’s try them out to
make sure:

>>
environment
=
{
x
:
3
}
=> {:x=>3}
>>
proc
=
eval
(
Add
.
new
(
Variable
.
new
(
:x
),
Number
.
new
(
1
))
.
to_ruby
)
=> #
>>
proc
.
call
(
environment
)
=> 4
>>
proc
=
eval
(
LessThan
.
new
(
Add
.
new
(
Variable
.
new
(
:x
),
Number
.
new
(
1
)),
Number
.
new
(
3
))
.
to_ruby
)
=> #
>>
proc
.
call
(
environment
)
=> false

Other books

Accepting Destiny by Christa Lynn
Taming Casanova by MJ Carnal
Boundary Lines by Melissa F. Olson
The Light of Asteria by Isaacs, Elizabeth
Greenwich by Howard Fast
Raven by Monica Porter