From kragen at pobox.com Thu Feb 8 03:37:02 2007
From: kragen at pobox.com (Kragen Javier Sitaker)
Date: Thu Feb 8 03:37:03 2007
Subject: naming of "self" in Bicicleta
Message-ID: <20070205201712.CD570E3413B@panacea.canonical.org>
So far I've been writing Bicicleta/sigma-calculus code with a separate
self-name per method:
point = {
x = 3
y = 4
point.r = sqrt((point.x * point.x) + (point.y * point.y))
point.theta = atan2(point.x, point.y)
}
But there's no power reason for each method to have a separate name
for "self"; if you pick a fresh name, you can safely use it for all
the methods. Specifying the name on each method makes the code more
verbose, and using different names on different methods makes the code
obscure.
So, inspired by OCaml, I am going to use a new notation that avoids
repeating the name:
point = {|p|
x = 3
y = 4
r = sqrt((p.x * p.x) + (p.y * p.y))
theta = atan2(p.x, p.y)
}
I can change the notation for display, say in case of inheritance:
cylindrical_point = point { |self|
z = 6.25
r = sqrt((self.x * self.x) + (self.y * self.y) + (self.z * self.z))
}
whose value displays as
cylindrical_point = point { |self|
z = 6.25 # 6.25
r = sqrt((self.x * self.x) + (self.y * self.y) + (self.z * self.z))
# 8.0039053
# inherited from point:
x = 3 # 3
y = 4 # 4
theta = atan2(self.x, self.y) # 0.6435011
}
This change in the display of theta, I think, will somewhat clarify
the nature of inheritance to the novice.
The presentation of the self-name on the screen in the IDE may be
something slightly different from vertical bars around the name.
From kragen at pobox.com Mon Feb 12 03:37:01 2007
From: kragen at pobox.com (Kragen Javier Sitaker)
Date: Mon Feb 12 03:37:03 2007
Subject: rational number class in Bicicleta's language
Message-ID: <20070211234517.EC9A7E340AA@panacea.canonical.org>
Here I present a short (one-page) class in Bicicleta for exact
arithmetic on rational numbers, followed by a ten-page explanation of
how it works. It's loosely based on the example at the beginning of
chapter 2 of SICP.
It is intended to stand alone, comprehensible without reference to any
of the previous Bicicleta posts I have made on kragen-tol and
kragen-hacks.
Unfortunately, I still don't have a runnable Bicicleta system, which
has several effects on this class:
- it probably has some bugs that will be obvious when I try to run it;
- I did not write unit tests, since I would not be able to run them;
- the textual source code does not have a full complement of example
values in it.
- it's about fractions, which are boring, instead of something cool
like colors or 3-D solid models or web sites.
The textual syntax in this mail message is not quite the syntax you
will see when you're editing the program. Because it's so important
to be able to exchange and discuss program fragments in purely textual
media like email, I plan to fully support copy and paste in this
textual format from the Bicicleta environment, without losing any
semantics; and because it's so important to integrate with
source-control systems, I plan to use the textual format as the
on-disk persistent storage format for Bicicleta programs. But within
the Bicicleta environment itself, the UI for interacting with the
program is not purely textual.
(In itself, this is not a large difference from traditional IDEs like
Eclipse.)
The other important thing to know is that Bicicleta, like Haskell,
pervasively uses lazy evaluation: no expression is evaluated until its
value is needed.
Things that embarrass me are marked with "***".
The Whole Class
---------------
First, here's the class all at once. I've followed it with a series
of piece-by-piece explanations.
rational = prog.sys.number {self:
numer = 1
denom = 2
new = {op:
arg1 = 6
arg2 = 9
g = op.arg1.gcd(op.arg2)
numer = op.arg1 / op.g
denom = op.arg2 / op.g
'()' = (op.numer * op.denom).if_not_error(
self { numer = op.numer, denom = op.denom })
}
show = "{numer}
{denom}" % self # presentational form definition
to_rational = {op:
arg1 = 2
'()' = prog.if(
op.arg1.denom.is_ok -> op.arg1
op.arg1.is_a(prog.sys.integer) -> self.new(op.arg1, 1)
else = prog.error()
)
}
rational_binary = {m:
arg1 = prog.sys.number.'+'
'()' = m.arg1 {op: 3, other = self.to_rational(op.arg1)}
}
# prog.sys.number.'+' returns 'result' unless it's erroneous, so we
# override 'result'
'+' = self.rational_binary(prog.sys.number.'+') {op:
result = self.new(
(self.numer * op.other.denom) + (self.denom * op.other.numer)
self.denom * op.other.denom
)
}
# The theory is that the standard number types' implementations of
# '+', '-', and the like, if the normal implementation fails with
# an error, will try passing themselves to 'reverse +', 'reverse
# -', and so on, instead. The override here is to stop the
# recursion.
'reverse +' = self.'+' {op: '()' = op.result}
negate = self.new(-self.numer, self.denom)
'-' = self.rational_binary(prog.sys.number.'-') {op:
result = self + op.other.negate
}
# This should not call '-', because if it does, and '-' is somehow
# broken, we get infinite recursion.
'reverse -' = self.'+' {op: '()' = self.negate + op.other}
'*' = self.rational_binary(prog.sys.number.'*') {op:
result = self.new(self.numer * op.other.numer,
self.denom * op.other.denom)
}
'reverse *' = self.'*' {op: '()' = op.result}
recip = self.new(self.denom, self.numer)
'/' = self.rational_binary(prog.sys.number.'/') {op:
result = self * op.other.recip
}
'reverse /' = self.'+' {op: '()' = self.recip * op.other}
# Equality is a really tricky operation. Here we're implementing
# numerical equality, but I'm not sure that's the right thing.
'==' = self.'+' {op:
'()' = (self.numer * op.other.denom) == (self.denom * op.other.numer)
}
}
rational = prog.sys.number {self: ... }
---------------------------------------
This says "'rational' inherits from prog.sys.number, with the
following differences: ...". The identifier "self" as the first thing
in the {} and followed by a ":" gives a name by which methods in
'rational' can refer to the object on which they are called.
(Sometimes I refer to methods as 'fields' in what follows.)
'prog' is the name given to the top-level namespace of the program.
Several of the fields introduced inside this expression do not exist
in prog.sys.number. (I'm not sure exactly which ones, but several of
them.) In Abad? and Cardelli's ?-calculus, introducing new fields in
an override expression like this is against the rules, but I am
disregarding this restriction.
numer = 1, denom = 2
--------------------
This defines two methods on the 'rational' object; one of them returns
1, and the other returns 2.
These are intended to be overridden in other objects derived from
'rational', but they serve as example values that allow the
"archetypal" or "prototypical" rational object to be viewed as a real
rational number, in this case 1/2. This allows the environment to
constantly display the effects of your changes to the code in real
time.
new = {op:...}
--------------
This defines a field (or method) whose value is another object, which
is intended as a constructor for new 'rational' objects. Within the
body of its definition, the name 'self' still refers to the 'rational'
object on which 'new' was called, and 'op' refers to the 'new' object
itself.
arg1 = 6
arg2 = 9
This defines two fields whose use becomes apparent later.
g = op.arg1.gcd(op.arg2)
This is syntactic sugar for the following definition:
'g' = op.'arg1'.'gcd'{'arg1' = op.'arg2'}.'()'
by way of the following translations:
1. It is allowed to leave off the '' around the name of a field
when the field name contains only alphanumeric and underscore
characters; (The '' allow the use of any arbitrary characters
in field names.)
2. In general x(y) is syntactic sugar for x{y}.'()'. Here 'x' is
some object, 'y' specifies some set of overrides on x to make
a derived object, and .'()' extracts the '()' field of the
resulting object.
3. Positional arguments in an override expression are treated as
overrides for the fields 'arg1', 'arg2', and so on.
Note that rule 3 means that you can write 'rational.new(2, 5)' to mean
'rational.new{arg1=2, arg2=5}'.
This says that the 'g' method on the 'new' object does the following:
1. extracts the 'arg1' field from the 'new' object on which it is
called;
2. extracts the 'gcd' field from that 'arg1' object;
3. extracts the 'arg2' field from the same 'new' object;
4. creates an object derived from the 'gcd' object in step 2 by
overriding its 'arg1' method to return the object from step 3;
5. extracts the '()' field from the resulting object.
'gcd' is a method defined on integer objects; its '()' field contains
its "return value", which is the greatest common divisor of the number
on which it is called and its argument. In this case, it is 3, but in
an object derived from 'new', arg1 and arg2 may be overridden, in
which case 'g' will have a different value.
Here is the current sugar-free notation for this field definition:
g = op.arg1.gcd{arg1 = op.arg2}.'()'
I used to write it as this instead:
op.g = op.arg1.gcd{arg1 = op.arg2}.'()'
This is more self-contained; but I concluded that it made more sense
to provide a single "self-name" for all the methods in a particular
expression, and I think the above was confusing to read, because most
readers did not guess that the first occurrence of 'op' introduced a
new binding for 'op' rather than referring to an existing binding. If
I recall correctly, in the notation of Abad? and Cardelli, it is
written like this:
g = ?(op) op.arg1.gcd{arg1 <= op.arg2}.val
which I usually expand in ASCII like this:
g = sigma(op) op.arg1.gcd{arg1 <= op.arg2}.val
where we write 'val' for '()' because they don't have a notation for
non-alphanumeric method names.
Note that the above does not constrain arg1 and arg2 to be integers;
arg1 just has to have a 'gcd' field which has a '()' field.
numer = op.arg1 / op.g
denom = op.arg2 / op.g
These divide the arg1 and arg2 by the gcd, giving 2 and 3
respectively. They are syntactic sugar for definitions like this:
numer = op.arg1.'/'(op.g)
which in turn is sugar for
'numer' = op.'arg1'.'/'{arg1 = op.'g'}.'()'
Any sequence of characters from this sequence will be interpreted in
this fashion as an infix operator:
~`!@$%^&*-+=<>?/\|
So you can define a ** operator or a += operator or a @!@@ operator by
defining fields on your objects with those names. There is no
precedence among these infix operators, and they all associate from
left to right; but, probably needless to say, the built-in "." and
override syntax bind more tightly than these infix operators, and the
separation between items expressed by "," or a line break binds more
loosely.
'()' = (op.numer * op.denom).if_not_error(
self { numer = op.numer, denom = op.denom })
This field, by convention supported by syntactic sugar, holds the
"return value" of 'new' --- so if you say 'rational.new(3, 4)' you are
getting the contents of this '()' field.
The 'if_not_error' method is defined on all objects. For normal
objects, it just returns its argument:
if_not_error = {op: '()' = op.arg1}
But for error objects, which result from calls to nonexistent methods
(among other things), it returns the error object itself:
if_not_error = { '()' = self }
The intent of the if_not_error call is to catch cases where op.numer,
op.denom, or both, are error objects, or where they are not the sort
of thing you can use as rational numerators or denominators. If
op.numer is an error object, its '*' method will just return another
error object; and the intrinsic '*' method defined for integers will
also return an error if passed an error object as an argument.
Finally, if op.numer is some kind of thing that doesn't have an '*'
method, or whose '*' method cannot accept op.denom as an argument, we
will get an error object reporting this, instead of a rational
number. (Being able to multiply together numerators and denominators
is a necessary condition for arithmetic on rational numbers, and it
should catch nearly all cases where the wrong thing was passed in.)
Error objects propagate along dataflow paths, as in VisiCalc and other
spreadsheets, rather than control-flow paths, as exceptions do in CLU
or Java. This is in part because the control-flow paths in a lazy
program are very hard to understand, but my experience with such a
mechanism in Wheat leads me to believe that this is a reasonable
approach for an imperative language as well, as long as there's an
escape hatch for error values evaluated in void context "for effect",
so they don't get lost.
This expression is a horizontal layout form:
self { numer = op.numer, denom = op.denom }
It's exactly equivalent to this:
self {
numer = op.numer
denom = op.denom
}
This constructs an object similar to the one on which 'new' is being
called, but with different 'numer' and 'denom' fields.
At one point in the past, I argued that constructions like this,
where the kind of object you instantiate covaries with the kind
of object you were operating on, were dead wrong. I was wrong.
This points out that you don't have to construct rationals through the
'new' method. You could also say "rational { numer = 3, denom = 4 }"
and have a perfectly usable 'rational' object.
The reason I'm providing the 'new' method is that it provides a useful
level of indirection. "rational { numer = 3, denom = 4 }" cannot
return an error object, nor rewrite numer and denom to lowest terms
the way 'new' does; and I found experimentally that it's quite easy to
override the wrong fields and introduce a subtle bug.
It's worth noticing that you could use 'new' to construct rationals
from any kind of object that supports 'gcd', '/', and '*' methods that
interact in the expected way; and, if they support the other ways that
numerators and denominators are used in this class (adding their
products together, negating them, testing for equality), they will
work, too. For example, I think you could use this class unchanged to
support rational expressions of polynomials.
show = "{numer}
{denom}" % self
------------------------------------
By convention, 'show' defines an HTML representation for the number
using the '%' method of strings, which uses reflection to extract the
fields named inside {} and interpolate them into a string.
Traditionally, programming languages have ways to render their objects
into ASCII strings, for debugging purposes if nothing else, but I
think HTML has supplanted ASCII text today. "show" is what the
Bicicleta environment uses to display the current values of the
objects you're editing.
I anticipate that for many classes, "show" or something like it will
suffice as a user interface.
*** This is not the right way to generate HTML. I probably want
something like Nevow stan or MochiKit.DOM, such as
"prog.html(self.numer, prog.html.hr, self.denom)", but I need more
experience to design that.
'+' = self.rational_binary(...) {op: ...}
-----------------------------------------
'+' = self.rational_binary(prog.sys.number.'+') {op:
result = self.new(
(self.numer * op.other.denom) + (self.denom * op.other.numer)
self.denom * op.other.denom
)
}
This defines a method for '+', which inherits from
self.rational_binary(prog.sys.number.'+'), which (as we'll see below)
inherits from prog.sys.number.'+'. Rather than defining '()', which
is what is actually used in an expression like "r1 + r2" (remember,
that rewrites to "r1.'+'{arg1=r2}.'()'") we define 'result', which the
'()' we inherit from prog.sys.number.'+' will return under most
circumstances.
Due to the lack of operator precedence in Bicicleta, we can't just
write "numer * other.denom + denom * other.numer"; that would parse as
"((numer * other.denom) + denom) * other.numer". So there's a set of
parentheses on the right to make it parse correctly, and another one
on the left for symmetry and clarity.
'other', as explained below, comes from 'rational_binary' and contains
a rational-number version of the argument, whether or not the argument
was originally a rational number.
to_rational = {op: ... '()' = prog.if(...)}
-------------------------------------------
to_rational = {op:
arg1 = 2
'()' = prog.if(
op.arg1.denom.is_ok -> op.arg1
op.arg1.is_a(prog.sys.integer) -> self.new(op.arg1, 1)
else = prog.error()
)
}
Most of the rest of the class is concerned with arithmetic. It's
desirable to be able to mix rational numbers with integers in
arithmetic; so this method returns a rational number, given either a
rational number or an integer.
The '->' method is defined for all objects; it returns an
'association' containing the object it was called on and its argument.
'if' is defined at 'prog', the top level; it implements a multi-way
conditional similar to Lisp's "cond". You pass it any number of
associations as arguments, and it iterates over them until it finds an
association whose left-hand side is true, at which point it returns
the right-hand side. If none of them are true, it returns 'else', in
this case, an error object. (*** I probably ought to provide an error
message.)
Note that the definition of 'if' in this fashion requires access
to the whole list of positional parameters, not just particular
individual positional parameters.
Because the language is lazy, 'if' can be just an ordinary function
rather than a language special form.
It is unfortunate that I have to write "prog.if" rather than "if" in
the textual syntax. This is because only the objects textually
enclosing an expression are bound to names in its scope; in this
expression, if 'rational' is evaluated at the top level 'prog', the
environment consists only of 'prog', 'self', and 'op'. I anticipate
that the Bicicleta environment will abbreviate these names for display
where doing so will not lead to reader confusion:
to_rational = {op:
arg1 = 2
'()' = if(
arg1.denom.is_ok -> arg1
arg1.is_a(sys.integer) -> new(arg1, 1)
else = error()
)
}
'is_ok' is a method defined on all objects; it returns true for all
objects except for error objects. If arg1 is a rational number, then
unless its denom is an error object, arg1.denom.is_ok will be true.
But for almost all objects other than rational numbers,
arg1.denom.is_ok will be false.
This is an example of using a 'protocol test' rather than an is-a or
isinstance test. (See my web page, "isinstance considered harmful".)
This leads to looser coupling and more maintainable systems.
Unfortunately I'm not sure how to do a protocol test to distinguish,
say, integers, which can be converted to exact rational numbers in the
way suggested above, from, say, floating-point numbers, which cannot.
Probably it will be clear how to do this after I've implemented some
of the more basic kinds of numbers. But in the mean time, I've fallen
back on "op.arg1.is_a(prog.sys.integer) -> self.new(op.arg1, 1)".
rational_binary = {m:
arg1 = prog.sys.number.'+'
'()' = m.arg1 {op: 3, other = self.to_rational(op.arg1)}
}
The methods that want to convert something else to a rational number
are all binary arithmetic methods of one kind or another, and the
thing they want to convert to rational is their argument. The trouble
is, for other reasons explained below, they need to inherit from their
corresponding methods in prog.sys.number, and because Bicicleta has
neither multiple inheritance nor super and therefore we cannot simply
implement "other = to_rational(arg1)" as a mixin.
However, this method demonstrates that it's straightforward to get
mixin-like functionality from a function. This function adds arg1=3
and the "other" method to its argument, and returns it.
In retrospect, this method probably didn't make the code much simpler,
but I've left it in because it demonstrates this important point about
the need for mixins.
*** Perhaps the "try to coerce to my type" behavior should be
inherited from prog.sys.number operations instead of implemented in
every numeric type.
At this point we have all the pieces to know that rational.'+'() is
7/2, because the default argument provided by rational_binary is 3.
*** I'm not sure about whether I should be providing reasonable
default arguments like this, which makes the code clearer when you're
editing it and seeing operational results, or defaulting to error
values so that when you use the class you get error messages instead
of silent wrong answers. I'm thinking that perhaps the first is
better during initial development, while the second is better later
on.
'reverse +' = self.'+' {op: '()' = op.result}
Earlier I said that prog.sys.number.'+' would return 'result' under
"most circumstances". I meant that it does the following:
'+' = {op: '()' = op.result !! op.arg1.'reverse +'(self)}
The '!!' error-handling operator comes from Wheat. It's like '||',
but for non-broken-ness rather than truthiness. In normal objects,
it's defined as follows:
'!!' = { '()' = self }
But in error objects, it's defined as follows:
'!!' = {op:
'()' = op.arg1 !! self.augment_err("trying to recover from", op.arg1)
}
So, if 'result' isn't an error, '+' returns it; if it is, it tries
arg1.'reverse +'(self), and if that isn't an error, it returns that;
and if they're both errors, it returns an error containing both
errors.
*** That won't actually work as written because self.augment_err
returns an error object. It should probably be written out as an if.
In this case, the way this works is that in "1 + rational.new(1, 2)",
we try 1.'+', which presumably will try and fail to coerce the
rational number to an integer; then it will fall back on
"rational.new(1, 2).'reverse +'(1)".
At first I tried this:
'reverse +' = self.'+'
This works in the normal case, where you're trying to add two rational
numbers, or a rational number and an integer (in either order). But
if there's an error (say, if you try to add 0.5 to a rational number)
then our 'reverse +' operator goes right ahead and tries to call
0.5.'reverse +' again. Which is OK, because that fails, and we get
a relatively sensible error.
But if there's some kind of bug in rational.'+' that makes 'result'
always an error for two particular rationals, then we'll fall into an
infinite recursive loop trying to add them, as they futilely swap back
and forth trying to find a configuration that works. This will result
in an error message that is considerably less helpful than it could
be.
So we inherit 'reverse +' from rational.'+', but we override '()' to
return 'result' directly instead of trying to reverse places again.
negate = self.new(-self.numer, self.denom)
This returns a negative version of the number (unless it's already
negative, in which case it returns a positive version). Its value is
-1/2 in the prototypical rational object.
*** The "-" in here doesn't fit into the syntactic structure I
described earlier for infix operators, and as a consequence, there's
no obvious way to override it. I am inclined to write this as
"self.numer.negate" instead and banish prefix operators completely
from the language.
'-' = self.rational_binary(prog.sys.number.'-') {op:
result = self + op.other.negate
}
'reverse -' = self.'+' {op: '()' = self.negate + op.other}
'-' overrides 'result' because '()' inherits a fallback to 'reverse -'
from prog.sys.number.'-', and 'reverse -' overrides '()' rather than
'result' in order to avoid falling back.
I inherited 'reverse -' from self.'+' to avoid having to write out the
rational_binary or to_rational conversion again (and potentially
forgetting). This has the perverse side effect that 'reverse -'
inherits an unused 'result' field containing the sum of 'self' and
'other'.
The 'reverse -' could still lead to an infinite recursive loop if
someone decided to implement '+' in terms of '-' (perhaps in some
other class), but it's not likely.
*** It would be nice if new programmers didn't have to deal with this
level of subtlety just to define a new numeric type. They don't in
Python. Maybe a level of indirection like "rational.new" could help?
Maybe something you could wrap around the whole class, like
"rational.rational_binary" wraps around a single operation.
'*' = self.rational_binary(prog.sys.number.'*') {op:
result = self.new(self.numer * op.other.numer,
self.denom * op.other.denom)
}
'reverse *' = self.'*' {op: '()' = op.result}
Perhaps I should define a 'commutative_reversed' method so that I
could say 'reverse *' = self.commutative_reversed(self.'*').
recip = self.new(self.denom, self.numer)
'/' = self.rational_binary(prog.sys.number.'/') {op:
result = self * op.other.recip
}
'reverse /' = self.'+' {op: '()' = self.recip * op.other}
Follows the same patterns as previously described for '-'.
'==' = self.'+' {op:
'()' = (self.numer * op.other.denom) == (self.denom * op.other.numer)
}
This definition is tricky because there are lots of different kinds of
equality: numerical equality, structural equality, and identity
equality are the ones we have to worry about here. Here I've picked
numerical equality as the definition for rational.'==', with the
consequences that "rational.new(2, 1) == 2" is true, and
"rational.new(2, 1) == 0.1" returns an error instead of false.
If our 'new' constructor (or gcd?) were smart enough to ensure that
denom was never negative, perhaps we could reduce this to "(numer ==
other.numer) && (denom == other.denom)".
*** '==' probably should use the same coercion techniques as '+' and
family if we're interested in numerical equality, so that we can test
"2 == rational.new(2, 1)".
The Whole Error Object Protocol
-------------------------------
Bicicleta has Wheat-style error objects, instantiated with
prog.error(), but they're just ordinary objects that implement only a
few methods:
- is_ok = false (true for other objects)
- if_not_error = {'()' = self} (arg1 for other objects)
- '!!' = {op: '()' = op.arg1 and a bit more} (self for other objects)
- show: something handy with hyperlinks
- get_error_info: returns the information about the object
There are some other methods I haven't really defined, like the
"augment_err" mentioned above. Possibly I should hide them behind an
"as_error" method.
Additionally, when you call a nonexistent method on an error object,
the error object returned isn't a fresh "nonexistent method called"
error object; it's the original error object, augmented with
information about being propagated through that method call. This is
achieved through some facility I haven't defined yet, analogous to
Smalltalk's "doesNotUnderstand:" or Python's "__getattr__".
Wheat's error objects modify themselves in place when they are passed
around. This became confusing in practice; in Bicicleta, error
objects have a 'augment_err' method that returns a copy of the same
error object, but with more trace information hanging off of it.
Is Succinctness Power?
----------------------
If you compare the above to the version that heads SICP's Chapter 2,
you will notice that it is substantially longer, by a factor of two to
four. I love brevity less than Paul Graham or Arthur Whitney, but I
certainly recognize brevity as precious, and I was dismayed when I
first realized how verbose Bicicleta code was.
Since that dismay, I have improved it somewhat, and now I think it's
roughly competitive with Scheme. Most of the extra length comes from
the hassles of integrating smoothly into an existing arithmetic
system, which isn't even possible in Scheme (in the abstract, that is
--- I think it's possible in, say, RScheme or PLT Scheme.)
It's still a somewhat fluffier language, mostly because it does many
things by name that Scheme does positionally.
Here's a version of the 'rational' class that's missing all the
extraneous code, focusing only on the basics:
rational = {r:
numer = 1
denom = 2
new = {op:
arg1 = 6
arg2 = 9
g = op.arg1.gcd(op.arg2)
numer = op.arg1 / op.g
denom = op.arg2 / op.g
'()' = (op.numer * op.denom).if_not_error(
r { numer = op.numer, denom = op.denom })
}
show = "{numer}
{denom}" % r
'+' = {op:
'()' = r.new(
(r.numer * op.arg1.denom) + (r.denom * op.arg1.numer)
r.denom * op.arg1.denom
)
}
negate = r.new(-r.numer, r.denom)
'-' = {op: '()' = r + op.arg1.negate}
'*' = {op: '()' = r.new(r.numer * op.arg1.numer, r.denom * op.arg1.denom)}
recip = r.new(r.denom, r.numer)
'/' = {op: '()' = r * op.arg1.recip}
'==' = {op: '()' = (r.numer * op.arg1.denom) == (r.denom * op.arg1.numer)}
}
That's 26 lines, 812 characters.
And here's a version abbreviated in the way I think things will
normally be abbreviated for display, where the namespace name is
omitted for uniquely-named methods:
rational = {r:
numer = 1
denom = 2
new = {op:
arg1 = 6
arg2 = 9
g = arg1.gcd(arg2)
numer = arg1 / op
denom = arg2 / op
'()' = (op.numer * op.denom).if_not_error(
r { numer = op.numer, denom = op.denom })
}
show = "{numer}
{denom}" % r
'+' = {
'()' = new(
(numer * arg1.denom) + (denom * arg1.numer)
denom * arg1.denom
)
}
negate = new(-numer, denom)
'-' = {'()' = r + arg1.negate}
'*' = {'()' = new(numer * arg1.numer, denom * arg1.denom)}
recip = new(denom, numer)
'/' = {'()' = r * arg1.recip}
'==' = {'()' = (numer * arg1.denom) == (denom * arg1.numer)}
}
That's 26 lines, 720 characters.
By comparison, the SICP version is 35 lines, 788 characters, but it
has some duplication of function; if I factor it and format it in an
analogous way, it's 18 lines, 710 characters.
Some parts of the Scheme version are actually more verbose than their
Bicicleta counterparts, which is why it's possible to come so close.
Consider:
negate = new(-numer, denom)
negate = r.new(-r.numer, r.denom)
negate = self.new(-self.numer, self.denom)
negate = self.new(self.numer.negate, self.denom)
(define (neg-rat x) (make-rat (- (numer x)) (denom x)))
'-' = {op: '()' = r + op.arg1.negate}
(define (sub-rat x y) (add-rat x (neg-rat y)))
So at this point I no longer have a lot of Scheme envy. Scheme still
wins by a little bit most of the time on brevity-in-the-small and
clear applicative-order control flow, but I think Bicicleta will win
on infix notation, named arguments, better namespace management,
runtime inspectability, and easier data structuring. Compared to
minimal standard schemes, Bicicleta will also win on late binding and
composability --- the SICP example rational class can only use
built-in numbers for its numerator and denominator.
The biggest recent improvement in brevity was not having to write the
self-name on every method that used it:
self.negate = self.new(-self.numer, self.denom)
Things Not Shown
----------------
There are a couple of things that are part of the Bicicleta design
that haven't made an appearance here.
The biggest one is editable presentation forms. When I'm editing my
source, I don't want rational numbers to display as "rational.new(3,
4)"; I want them to display like this:
3
---
4
But not a hokey dashed line made out of hyphens --- a solid line, with
a minimal amount of space above and below it. I might want to switch
to "text view" to see how to make another one (the big advantage of
text is that it makes the gulf of execution quite small), but I should
be able to change the 3 to a 5 without going to that extreme.
The handling of side effects is something I have given some thought
to, but which doesn't show up here. My plan is to make a special kind
of function called a 'procedure'; to only allow procedures to be
called from procedures; to cause UI elements to invoke procedures; to
cause the 'main' function of a standalone program to invoke a
procedure; to provide special procedures that implement iteration of a
procedure and sequencing of multiple procedures; to use 'if' to
implement a choice between N procedures (it just returns the procedure
it wants to call); and, generally, to record the last value returned
from a procedure so it can be inspected without being re-executed.
Another, much smaller, item is that "x[y]" is syntactic sugar for
"x.'[]'(y)", as in Wheat; this is used for indexing into data
containers. This (plus conventions for escaping in strings) completes
the demonstration of the textual language grammar.
From kragen at pobox.com Thu Feb 15 03:37:01 2007
From: kragen at pobox.com (Kragen Javier Sitaker)
Date: Thu Feb 15 03:37:03 2007
Subject: logical versus physical "logging" in network protocol design
Message-ID: <20070205201712.73C57E340FA@panacea.canonical.org>
Database systems normally use one (or both) of two different systems
to record transaction changes for later rollback or commit: "logical
logging", where the change is recorded, and "physical logging", where
the before and after states of the affected part of the table are
recorded. For example, inserting a row into a table might result in a
single logical-log entry with the inserted values, or it might result
in one or more physical-log entries with the new contents of the table
page where it was inserted, and possibly other table pages that it
pushed other records into.
This distinction is not limited to database transaction recovery logs;
it applies to any kind of persistence at all. For example, you can
save the contents of a text file either in the form of each of the
physical lines you want to see on the screen, in the order that you
want to see them, or in the form of a sequence of insertions and
deletions (and maybe moves) to get from an empty file to the current
contents of the text file. That's roughly how darcs works.
In the case of persistence, the tradeoff is fairly simple: storing the
current state of things is simpler, uses an amount of space that does
not grow over time, potentially uses more I/O bandwidth for small
changes (you have to rewrite the whole file to insert a line at the
beginning), but it doesn't allow you to go back in time to recover
from problems or to learn more about the history of the file.
This distinction also applies to network protocols. If there's some
piece of state that's maintained in one location, and other things are
accessing that state across the network to change it, you can either
transmit the new state, or the desired change to the state. In this
case, the tradeoffs are somewhat more complex.
"High REST" comes down firmly on the side of transmitting the new
state, in an HTTP PUT or DELETE request. The reason given is that, if
you never get a response (say, because you get out of range of the
Wi-Fi access point), it might be that your request was lost, or it
might be that the server handled it. So it's nice to be able to
retransmit the request automatically, in order to deal with failures
like this, and you can do that with new-state-transmission requests,
because they are idempotent.
And, if you're the only person who might possibly be changing the
state of the resource, or anything else that the resource's state
might semantically depend on, that's all the concurrency control you
need, and that's really the right thing.
(In some non-HTTP contexts, there's another potential advantage to
this approach: it's possible for intermediaries to drop some update
messages if they know they have been superseded by other updates for
the same resource. Linux's "epoll" family of system calls, for
example, uses a variant of this approach to ensure that it never has
to lose a notification.)
But if other things in the universe might change the state of the
resource --- for example, a timeout, or another person, or yourself on
another machine with a working network connection --- you need some
way to ensure that the change still makes sense at the time the server
applies it.
These concurrency-control mechanisms essentially fall into the two
categories of pessimistic and optimistic concurrency control;
optimistic concurrency control rejects your change request if its
preconditions are not met, probably because something changed before
the change request reached the server, and pessimistic concurrency
control allows you to "acquire locks" to ensure that the preconditions
stay true for some period of time so that your change request is
guaranteed to be sensible. Pure pessimistic concurrency control is
rarely sensible when the thing "holding the locks" might fail
independently of the thing the locks limit access to, which is why
it's usually backed up with some form of optimistic concurrency
control in distributed systems, for example, in the form of leases
that limit the lifetime of locks. Java uses pure pessimistic
concurrency control within the VM, which is why Thread.stop(Throwable)
is now deprecated.
There is really very little machinery for this kind of concurrency
control in HTTP. WebDAV [RFC 2518] adds locks with timeouts to HTTP,
and it is not a coincidence that WebDAV is one of the few widespread
uses of HTTP PUT.
The more common practice on the Web, sometimes known as "Low REST", is
to send the logical change across the network instead of the new state
of the resource --- in a POST request. This has the drawback that the
request cannot safely be retransmitted, since it may not be
idempotent.
However, it has the advantage that all the concurrency problems are
local to the server --- they don't appear in the network protocol at
all. The server can handle the update requests in a purely serial
fashion (this used to be common in the days of mSQL, and is now coming
back into style with FastCGI-based sites, many of them in Rails,
although it's less relevant now), rely on PostgreSQL transactions, use
Java's threads and locks, or whatever, but you still don't have to
deal with a heterogeneous distributed concurrency problem with large
latency and partial failure. Usually this is a win, especially for
small web sites. (If you need a server farm to run your web site, you
may still have to deal with some of these issues.)
By the same token, it puts all the code to construct new versions of
resources on the server side. Before about 2002, this was a big
advantage, since if you wanted to run something on the client side,
you had to choose between JavaScript, a language that didn't work, and
Java, a language that took a lot of work to do simple things in. On
the server side, on the other hand, you could run anything you liked.
Most people ran Perl, but a lot of things were in Python, Tcl, Visual
Basic, PHP/FI (as PHP was called then), and the like.
Most resources on web sites can be modified by more than one person,
and many of them often are modified by other people. On the other
hand, network failure is still fairly rare, and in the early days of
the web, before SLIP and PPP became widespread (let alone Wi-Fi), it
was very rare indeed. So it's rare that using PUT instead of POST
buys you anything.
The information transmitted in a POST is a logical-log-like request to
perform some human-comprehensible operation: to add a comment to a
blogpost, to set your rating for a user script, to update a certain
section of a Wikipedia page from a previous state to a new state.
(Notice the hybrid nature of this last one.) Because these operations
often remain meaningful even if other things change before they are
carried out, they offer more scope for concurrency and conflict
resolution than a physical-log-style PUT. More concretely, if two
people post comments on this blog post, while I add a category to it
and Ryan adds another category to it, those operations can all be sent
to the server without any of us seeing the other people's changes
ahead of time, and everything will just work.
A few years ago, I wrote about a speculative architecture for
distributed programs that I called "rumor-oriented programming". The
basic idea is that you record nothing persistently except for a
sequence of HTTP-POST-level changes, and then you define your program
in terms of SQL-like queries (perhaps with automatically-maintained
materialized views) on the entire history of all the user interface
actions; the back-end data store is append-only, so it demands little
in the way of sophisticated concurrency control. In theory, this
could make disconnected operation and synchronization work
automatically with little hassle. I've written one or two small
programs this way, and it does seem to work reasonably well; it
remains to be seen whether it would work well in larger programs
maintained over longer periods of time.
From kragen at pobox.com Mon Feb 19 03:37:02 2007
From: kragen at pobox.com (Kragen Javier Sitaker)
Date: Mon Feb 19 03:37:03 2007
Subject: installing Debian with debootstrap and LNX-BBC
Message-ID: <20070205201712.7D3F4E3410E@panacea.canonical.org>
I'm installing Debian on a machine remotely using debootstrap from
LNX-BBC. Here are my notes.
LNX-BBC automatically mounts your disks:
cd /mnt/rw/discs/disc0/part1
But we have to remount them read-write:
mount -no remount,rw .
Then download debootstrap according to
http://www.debian.org/releases/stable/i386/apcs04.html.en --- although
probably http://www.debian.org/releases/etch/i386/apds03.html.en is
better now! (These are chapters of the Debian GNU/Linux Installation
Guide.)
mkdir debootstrap
cd debootstrap
wget http://ftp.debian.org/debian/pool/main/d/debootstrap/debootstrap_0.3.3.1_all.deb
ar -x debootstrap_0.3.3.1_all.deb
Can't extract data.tar.gz in the root directory (read-only filesystem)
so I am hoping to use DEBOOTSTRAP_DIR to enable debootstrap to find it
here:
tar xzvf data.tar.gz
export DEBOOTSTRAP_DIR=/mnt/rw/discs/disc0/part1/debootstrap/usr/lib/debootstrap
usr/sbin/debootstrap --arch i386 etch /mnt/rw/discs/disc0/part1 \
http://http.us.debian.org/debian
I think this didn't succeed, because of this message:
I: Installing core packages...
W: Failure trying to run: chroot /mnt/rw/discs/disc0/part1 dpkg --force-depends --install var/cache/apt/archives/base-files_4_i386.deb var/cache/apt/archives/base-passwd_3.5.11_i386.deb
It looks like it may have failed because of a PATH problem:
[root@lnx-bbc:/mnt/rw/discs/disc0/part1/debootstrap]# chroot \
/mnt/rw/discs/disc0/part1 dpkg --force-depends \
--install var/cache/apt/archives/base-files_4_i386.deb \
var/cache/apt/archives/base-passwd_3.5.11_i386.deb
chroot: cannot execute dpkg: No such file or directory
That's because dpkg is in /usr/bin, and /usr/bin isn't in the $PATH on
LNX-BBC. So I repeat the process:
export PATH=/usr/bin:$PATH
usr/sbin/debootstrap --arch i386 etch /mnt/rw/discs/disc0/part1 http://http.us.debian.org/debian
Same problem, but this time when I try running dpkg by hand, it says:
dpkg: `install-info' not found on PATH.
dpkg: `update-rc.d' not found on PATH.
dpkg: 2 expected program(s) not found on PATH.
NB: root's PATH should usually contain /usr/local/sbin,
/usr/sbin and /sbin.
And in this case, the absence of /usr/sbin is the problem.
export PATH=/usr/sbin:$PATH
and this time I get
I: Base system installed successfully.
And I'm in:
[root@lnx-bbc:/mnt/rw/discs/disc0/part1/debootstrap]# LANG= chroot .. /bin/bash
So I can
apt-get update
apt-get install vim
vim /etc/fstab
and I put these in my fstab:
/dev/hda1 / ext3 defaults 0 1
/dev/hdb1 /bigdisk ext3 defaults 0 2
/dev/hdb2 /bigdisk2 ext3 defaults 0 2
proc /proc proc defaults 0 0
/dev/fd0 /mnt/floppy auto noauto,rw,sync,user,exec 0 0
/dev/cdrom /mnt/cdrom iso9660 noauto,ro,user,exec 0 0
I didn't include /sys, /dev, /dev/pts, /proc/bus/usb, /tmp, or
/dev/shm in the fstab because they aren't in it on my other Debian
system. I didn't include swap because there's no free space on the
disk, and I figure I can use a swapfile anyway. Later.
And then I
mount /proc
I'm kind of guessing about the device names, since LNX-BBC uses devfs,
but this seems to suggest that they are correct:
root@lnx-bbc:/# cat /proc/ide/ide0/hda/model
ST3200826A
root@lnx-bbc:/# cat /proc/ide/ide0/hdb/model
WDC WD800JB-00ETA0
Then
apt-get install console-data # and select qwerty, US American
And then I put this into /etc/network/interfaces:
auto lo
iface lo inet loopback
iface eth0 inet dhcp
Vim kept complaining about not being able to put stuff in
/home/root/.viminfo, which is under LNX-BBC's $HOME, so I did this:
ln -s /root /home/root
I'm pretty sure I'll need "alias eth0 8139too" in /etc/modules.conf or
whatever its modern equivalent is, because apparently the RealTek
8139-based Ethernet card that's in the machine doesn't get
auto-detected; from the LNX-BBC boot:
8139too Fast Ethernet driver 0.9.25
PCI: Found IRQ 5 for device 02:08.0
PCI: Sharing IRQ 5 with 00:02.0
eth0: SMC1211TX EZCard 10/100 (RealTek RTL8139) at 0x1000,
00:10:b5:ec:xx:xx, IRQ 5
eth0: Identified 8139 chip type 'RTL-8139C'
eth0: Setting 100mbps full-duplex based on auto-negotiated
partner ability 45e1.
eth0: no IPv6 routers present
First I have to figure out what the modern equivalent of modules.conf
is, though. For now:
echo 8139too >> /etc/modules
Then
echo gentle.murch-sitaker.org>/etc/hostname
tzconfig
vi /etc/hosts # and paste in the standard hosts, changing DebianHostName
vi /etc/apt/sources.list # and paste in the standard sources
aptitude update
aptitude install locales
dpkg-reconfigure locales # install all, select en_US.UTF-8
It was probably a mistake to install all locales, because it took a
long time to build them all. (The CPU on this old machine is fairly
slow.)
Then to install a kernel and a bootloader:
aptitude install kernel-package
aptitude install screen # so i can read a man page while editing the file
mount -t devpts devpts /dev/pts # to get screen to work
screen
vi /etc/kernel-img.conf # and I put the following in it:
do_symlinks = yes
relative_links = yes
do_bootloader = no
do_bootfloppy = no
warn_initrd = no
postinst_hook = update-grub
postrm_hook = update-grub
aptitude install kernel-image-2.6-386 # this failed due to no grub
aptitude install grub
mknod /dev/hda b 3 0
But then I ran into trouble:
lnx-bbc:/# grub-install /dev/hda
Probing devices to guess BIOS drives. This may take a long time.
Could not find device for /boot: Not found or not a block device.
So I thought maybe I'd strace it.
aptitude install strace
resulted in some more progress on the kernel front:
Could not find /boot/grub/menu.lst file. Would you like
/boot/grub/menu.lst generated for you? (y/N) y
At this point, it hung, and I started to get the idea that aptitude
really isn't better than apt-get after all. I control-Ced it, and it
installed strace and tried again. This time I had strace, but it
wasn't helpful to diagnose the hang.
The main thing strace told me about the grub-install problem was that
grub-install was a shell script. I read some of it, but it's 500+
lines long, so I gave up before understanding clearly what was going
on.
So I gave up on grub and decided to switch to lilo.
aptitude remove grub
vi /etc/kernel-img.conf # remove the *hook lines, set do_bootloader=yes
aptitude install lilo
vi /etc/lilo.conf # and put the following in it:
boot=/dev/hda
root=/dev/hda1
install=menu
delay=20
lba32
image=/vmlinuz
label=Debian
lilo # but this produces an error:
Warning: '/proc/partitions' does not match '/dev' directory structure.
Name change: '/dev/hdc' -> '/dev/hdc'
part_nowrite check:: No such file or directory
(XXX I am hoping that the "lba32" in there is correct.)
I am hypothesizing that this error is from /dev/hda1 not existing, so:
mknod /dev/hda1 b 3 1
lilo
This does seem to improve things:
Warning: '/proc/partitions' does not match '/dev' directory structure.
Name change: '/dev/hdc' -> '/dev/hdc'
Cannot proceed maybe you need to add this to your lilo.conf:
disk=/dev/hdb inaccessible
(real error shown below)
Fatal: open /dev/hdb: No such file or directory
I don't know why it thinks it should be touching /dev/hdb or /dev/hdc,
but I'll go ahead and let it:
mknod /dev/hdb b 3 64
mknod /dev/hdb1 b 3 65
mknod /dev/hdb2 b 3 66
mknod /dev/hdc b 22 0
lilo
And this time it seems to have been successful:
Warning: '/proc/partitions' does not match '/dev' directory structure.
Name change: '/dev/ide/host0/bus0/target0/lun0/disc' -> '/dev/hda'
Added Debian *
Now I exit from the screen and the chrooted shell in order to see my
disk-space status; there appears to be plenty of space left. So I try
the 'tasksel install standard' recommendation from the manual.
LANG= chroot .. /bin/bash
tasksel install standard
But this installs SELinux and Exim, among other things. So:
apt-get remove exim4 # doesn't work
apt-get remove exim4-base # works but uninstalls at, mailx, and mutt
apt-get remove selinux-policy-refpolicy-targeted
Then I needed to remember to install sshd:
apt-get install openssh-server
Then I noticed (via netstat -an) that there were some sockets, so I
lsof | grep TCP and | grep UDP to see who they were. They were
portmap and rpc.statd, so I removed them:
apt-get remove nfs-common portmap
That didn't work --- hung forever trying to stop the portmap daemon.
I ended up killing the process by hand, removing /sbin/portmap, and
then re-removing the package.
Then I figured I'd update the passwd, shadow, and group files so I
could log in; I merged the original versions with the newer ones.
Then I overwrote the stuff in /etc/ssh with the old versions:
lnx-bbc:/kragen/gentle-backup/etc/ssh# cp * /etc/ssh
Hope that's compatible!
Just to test, I used ps x | grep ssh to find the pids of the LNX-BBC
shell process, and then inside screen, I ran this command:
kill -9 641 666; /etc/init.d/sshd start
It worked in the sense that I now had an ssh server running in the
environment that I hoped would become the new system, but it was a
mistake in the sense that I hadn't yet installed sudo! So I asked my
friend with physical access to run these commands before we reboot,
which worked:
chroot /mnt/rw/discs/disc0/part1 apt-get install sudo
chroot /mnt/rw/discs/disc0/part1 visudo # and add me!
It didn't reboot properly --- LILO died with L 00 00 00 ... --- but
LBA wasn't enabled in the BIOS. While debugging this, my friend found
an error in the Portuguese translation of the BIOS. We gave up
debugging it for a while.
Because the machine was down for a long time, the DHCP server gave it
a different address, so I put the new address into DNS.
Now I have a problem; my server is running inside a chroot, but I
can't mount the partitions /bigdisk and /bigdisk2 inside the chroot
because they are already mounted outside of it; and I can't unmount
them because "umount" can't see them.
Apparently even the Linux 2.4.19 used by LNX-BBC does this clever
thing where you can't see /proc/*/cwd for processes chrooted somewhere
above you, so I can't "sudo chroot /proc/1/root bash". However, the
standard C approach to breaking out of the chroot does work (here
without error handling for brevity):
chroot(argv[1]);
chdir("../../../../../../../../../..");
chroot(".");
execl(argv[2], argv[2], 0);
I was going to do things the hard way and inject code into a process
running outside the chroot by means of gdb, but then I added
error-checking to the C code and got it working.
sudo ./unchroot flickr-interesting/ /bin/bash
# and then unmount things in the resulting root shell
sudo mount /bigdisk
sudo mount /bigdisk2
sudo /etc/init.d/chrooted-apache start # this is me-specific
And now my web server is back up and running, for now.
From kragen at pobox.com Thu Feb 22 03:37:01 2007
From: kragen at pobox.com (Kragen Javier Sitaker)
Date: Thu Feb 22 03:37:02 2007
Subject: novice's notes on learning OCaml
Message-ID: <20070205201712.955B5E34131@panacea.canonical.org>
I've just been learning OCaml for the first time; I've written OCaml
programs before, but I didn't really learn enough of the language to
do anything useful. I thought I would take some time to reflect on
the experience of learning the language, since (barring brain damage)
I won't have the opportunity to learn OCaml a second time.
First, the positives.
All Kinds Of Good Stuff
=======================
OCaml has many advantages. Even the bytecode interpreter, running
under PowerPC emulation on this Intel Mac, is much faster than
Python's interpreter or any JavaScript interpreter. You can define
new infix operations easily, or replace old ones (although you can't
override the old ones for a particular type). Pattern matching makes
all kinds of interpreter-like things a cinch. It's easy to use infix
operations as values (merely by parenthesizing them). Implicit
currying often makes programs briefer and sometimes even clearer.
User-defined constructors display nicely by default, and just in
general, tagged unions are nice to use.
Pattern-matching is especially nice; consider this:
let rec toyaplshow = ... function ...
| Parenthesized e -> "(" ^ toyaplshow e ^ ")"
| Binary (Atom _ as e1, f, e2) | Binary (Parenthesized _ as e1, f, e2) ->
toyaplshow e1 ^ " " ^ f ^ " " ^ toyaplshow e2
| Binary (e, f, e2) ->
toyaplshow (Parenthesized e) ^ " " ^ f ^ " " ^ toyaplshow e2 ;;
Binary, Atom, and Parenthesized are three of the constructors for a
certain abstract type; toyaplshow is a recursive function for
displaying it as an expression; and "^" is string concatenation. In
this case, I want to parenthesize the leftmost operand of a binary
operation if it's not one of Atom or Parenthesized. Doing this in an
object-oriented style, the code is about twice as long.
class Expr:
def needs_parentheses_sometimes(self): return True
class Binary(Expr):
def toyaplshow(self):
left = self.left
if left.needs_parentheses_sometimes(): left = Parenthesized(left)
return "%s %s %s" % (left.toyaplshow(), self.op,
self.right.toyaplshow())
class Atom(Expr):
def needs_parentheses_sometimes(self): return False
class Parenthesized(Expr):
def needs_parentheses_sometimes(self): return False
def toyaplshow(self): return "(%s)" % (self.expr,)
In this case, there also happen to be more kinds of values than there
are operations on them, so dividing the operations along class lines
instead of along functional lines results in stronger coupling.
Things can get far hairier; consider this example from the OCaml tutorial:
let balance = function
Black, z, Node (Red, y, Node (Red, x, a, b), c), d
| Black, z, Node (Red, x, a, Node (Red, y, b, c)), d
| Black, x, a, Node (Red, z, Node (Red, y, b, c), d)
| Black, x, a, Node (Red, y, b, Node (Red, z, c, d)) ->
Node (Red, y, Node (Black, x, a, b), Node (Black, z, c, d))
| a, b, c, d ->
Node (a, b, c, d)
In theory, you can get access to all of the Perl and Python libraries
as well through FFIs to Perl and Python, but those FFIs don't seem to
have been included in the OCaml package I got.
As with Python and Perl, you get fairly full access to the POSIX API,
but with some improvements; for example, select() takes lists of
file-descriptor objects, gettimeofday() returns a float, and most of
the system calls raise exceptions when they fail instead of returning
an error code. I'm not sure all the changes are necessarily
improvements; I might really prefer bitvectors for select() for
efficiency, for example, and I'm not sure I can create a
Unix.file_descr from an integer. But mostly they are.
And there are optional and named arguments (which seem to mostly
work), an object system that supports fully-static duck typing (at
least as far as what methods your objects define), a profiler, a
debugger, a native-code compiler that is reputed to generate good
code, records with named fields, recursion, tail-call elimination,
etc. No first-class continuations, but exceptions. And the compiler
tells you if you left out a case in your pattern-matching case
analysis.
Some Bad Stuff Too
==================
I wrote most of this while working through Jason Hickey's tutorial;
these are my notes learning OCaml as an experienced programmer who
doesn't know much OCaml. As programming language tutorials go, it's
not too bad, although it's less than practical-minded; he doesn't
mention I/O at all until chapter 8, or explain it in full until
chapter 9.
I'm not that impressed with the OCaml experience for newbies.
Learning a new programming language is always somewhat frustrating; it
usually takes several days of learning before you can do anything
useful. But I've found OCaml particularly frustrating, despite its
many advantages, and I thought I would write a bit about the
frustrations in hopes that it might enable me to correct them later
on.
Some of these frustrations are deeply rooted in the nature of OCaml,
and are not likely to go away without a completely new language, and
will probably continue to bother me even when I know the language;
others, such as badly-worded error messages, are quite superficial.
I hope this doesn't attract flames; it's not intended to deprecate the
OCaml language, which I am very impressed with. I'm a little worried
that comparing OCaml unfavorably to déclassé languages like Perl,
Python, and JavaScript will lead people to discount my opinion. I
assure you, I know other programming languages well (C being my
favorite low-level language and one I know well), but I find Python
and JavaScript more practical than C most of the time.
Insufficiently Helpful Error Messages
-------------------------------------
Here's a typical newbie mistake. I mistakenly wrote the pattern
"(a, b) :: rest" as "a, b :: rest", thinking that would parse as I
intended (since, of course, [a, b; c, d] parses as [(a, b); (c, d)]).
The error message from the ocaml toplevel said:
This pattern matches values of type 'a * 'b list
but is here used to match values of type 'c list
Now, if you read the error message correctly, it says that I'm trying
to match a list (which ocaml knew because another pattern-matching
case was []) against a pattern for a tuple containing a list as a
second member. But the type "'a * 'b list" is structurally ambiguous
in the same way that the pattern is; I parsed it as ('a * 'b) list,
which was what I intended, not 'a * ('b list), which is what the
compiler meant.
In short, I was communicating ambiguously to the compiler, which
misinterpreted what I meant as something that didn't make sense; in
order to explain how it interpreted my statement, it used an
explanation that was ambiguous in exactly the same way, so that it
appeared to me that the compiler had understood my intent, but for
some mysterious reason did not think it was a good idea to carry out.
Of course, anyone who knows the least little bit of OCaml can parse
the type, and furthermore is not likely to seek explanations in
"mysterious reasons". But as I was just beginning to learn OCaml,
everything was somewhat mysterious to start with.
Another example:
function ([], []) -> 3 | (a1::as, b1::bs) -> 4 | _ -> 5 ;;
^
Syntax error: ')' expected, the highlighted '(' might be unmatched
This pattern-match doesn't parse. Why? I couldn't tell. The error
message doesn't help. (It turns out that "as" is a reserved word.)
Even "assert", which you put in your program so as to give you a
useful error message, gives you crap like this:
Exception: Assert_failure ("toyapl.ml", 62, 4).
It does tell you which assertion failed (eventually you learn that
that means "line 62, column 4") but it would be nice if it included
the string of the actual expression that failed.
Most of my assertions are unit tests, usually assertions of equality
between two things, an expected value and an actual value. I can
compare these two with the "=" operator, but I cannot figure out how
to declare an exception to tell me what they were:
exception Test_failure of int * 'a * 'a ;;
^^
Unbound type parameter 'a
I'd be satisfied with string representations; somehow the toplevel
produces string representations of objects of arbitrary types, but I
haven't yet learned how to do that.
Another example that's not quite so egregious:
let y = List.map ((+)2) up_to 2000000 ;;
^^^^^^^^
This function is applied to too many arguments,
maybe you forgot a `;'
In my experience so far, every time I have gotten this message, it
hasn't been because I forgot a ';'; it's been because I neglected to
include some parentheses.
Sometimes Completely Enigmatic Error Messages
---------------------------------------------
This expression has type toyaplexpr but is here used with type toyaplexpr
This can happen when you redefine a type in the interactive toplevel,
but some references to the old type remain. The enigmatic nature of
this error message is such a well-known bug that it is described in
many introductions to OCaml.
# Unix.getpid () ;;
Reference to undefined global `Unix'
This means that it can't access the "Unix" library, although it
doesn't bother to say why. It does not mean that the "Unix" library
doesn't exist, or that you misspelled its name. It means you needed
to launch the ocaml interpreter with "ocaml unix.cma" instead of
"ocaml".
Insufficiently Helpful Exceptions
---------------------------------
# List.assoc "hi" ["ho", 3] ;;
Exception: Not_found.
Good thing "hi" was a constant --- it would be hard to tell what was
"Not_found", because the Not_found exception doesn't take any
arguments.
Similarly:
# int_of_string "31 ";;
Exception: Failure "int_of_string".
It would be helpful if the exception told what string it failed on. I
think it may be impossible to add this in a backward-compatible way to
OCaml today.
Along the same lines, these exceptions fail to acquire any stack-trace
information; you get the same error message of two or three words if
the exception happened deep down the stack, in the middle of a big
program.
The default Python stack trace at least shows you each line of code in
the exception stack, and I usually use cgitb, which also shows you a
few lines of context and the values of all the variables on the line
of code in question. Usually this is sufficient to fix the problem
without further experimentation.
The lack of a stack trace on exceptions is hardly a disadvantage when
you're comparing to C (no exceptions) or C++ (no stack traces either),
but it's a disadvantage comparing to Java, Lisp, or Python; even Perl
records the source file and line number of origin by default, and a
full stack trace if you use "croak" instead of just "die".
Insufficient Introspection
--------------------------
So what does List.assoc do, anyway?
# List.assoc ;;
- : 'a -> ('a * 'b) list -> 'b =
Well, that's nice --- it tells you what the arguments and the return
type are. Maybe from those you can figure out what the function is
for. (In OCaml, that's not as far-fetched as it sounds; I can't think
of any other useful functions that have the type signature above. But
it takes some thought.) But if you type an analogous expression in R,
you get the entire source code of the function. In Python, the
default response is similarly unenlightening:
>>> map
But look, you can do this:
>>> help(map)
Help on built-in function map:
map(...)
map(function, sequence[, sequence, ...]) -> list
Return a list of the results of applying the function to the
items of the argument sequence(s). If more than one sequence
is given, the function is called with an argument list
consisting of the corresponding item of each sequence,
substituting None for missing values when not all sequences
have the same length. If the function is None, return a list
of the items of the sequence (or a list of tuples if more than
one sequence).
How about the functions available in the List library? The List
library isn't even a value that you can type as an expression:
# List ;;
Characters 0-4:
List ;;
^^^^
Unbound constructor List
Compare Python:
>>> import string
>>> dir(string)
['__builtins__', ... 'upper', 'uppercase', 'whitespace', 'zfill']
>>> dir()
['__builtins__', '__doc__', '__name__', 'string']
>>> dir(__builtins__)
['ArithmeticError', 'AssertionError', 'AttributeError', ...
'staticmethod', 'str', 'sum', 'super', 'tuple', 'type', 'unichr',
'unicode', 'vars', 'xrange', 'zip']
Python even has features like rlcompleter2 and IPython which allow you
to type "string." and hit Tab to see what's in the "string" module.
(What Python calls "modules", OCaml calls "libraries".)
This would be a smaller problem if the OCaml documentation were
complete and up-to-date, but see the section "Missing Documentation".
Too Much Precedence
-------------------
OCaml has a lot of syntax. Another typical newbie mistake:
let simpleenv = ["+", fun x -> x; "-", (~-)] ;;
Commonly, because "," binds tighter than ";", you can conveniently
write lists of pairs conveniently like [1, "one"; 2, "two"]. In this
case, though, the "fun" gobbles up everything to the right, as
explained by the type the interpreter spits out:
(string * ('a -> string * (int -> int))) list
Which is to say, a list of pairs of strings and functions, where each
function takes a value of some arbitrary type, discards it, and
returns a pair of a string and a function from ints to ints.
Somewhat Low-Level
------------------
The negative side of Ocaml's higher speed is that it's somewhat
low-level; the intrinsic data structures are things like linked lists,
fixed-size arrays, strings, and several different struct-like
structures (tuples, constructors, and records). There are libraries
that provide things like auto-growing string buffers, queues, sets,
hash tables, balanced binary trees (in case you're a masochist),
(Perl-incompatible) regular expressions, random number generation,
large multidimensional numerical arrays, type-unsafe data structure
marshaling, arbitrary-precision numbers, MD5, command-line parsing,
and so on.
However, it appears that because Ocaml's "module", functor, and
object-orientation facilities are not widely enough used, the
information about whether you're using, say, a hash table or a
balanced binary tree, will be spread through all the code that
accesses it.
Lame Libraries
--------------
It's not that the regexp library (in the Str module) is gratuitously
Perl-incompatible, or that it gratuitously uses global state; given
OCaml's age, that may be forgivable. It's just that the library is
small.
Some people see this as an advantage:
It's not that there are an extraordinary number of tools in the
toolbox. (No; in fact, the toolbox is much smaller than the usual
toolboxes, the ones used by your friends that contain everything
but the sink.) It's that the toolbox was carefully and very
thoughtfully assembled by some very bright toolsmiths, distilling
their many decades of experience, and designed, as all good
toolkits are, for a very specific purpose: building fast, safe and
solid programs that are oriented around separate compilation of
functions that primarily do recursive manipulation of very complex
data structures.
Programs, like, say ... compilers.
- Dwight VandenBerghe , 1998-07-28, "Why
ML/OCaml are good for writing compilers", posted to
comp.compilers, currently online at
http://flint.cs.yale.edu/cs421/case-for-ml.html
I don't see it as an advantage, myself. I'm glad OCaml isn't multiple
gigabytes like the Apple Developer Tools CD, but here I have listed
some of the things I hoped were there, but that I can't find in the
standard library shipped with the language. Most of the things listed
below have Python versions preinstalled on this Mac in
/System/Library/Frameworks/Python.framework (66MB), Perl versions in
/System/Library/Perl (45MB), and Java versions in
/System/Library/Frameworks/JavaVM.framework/Versions/1.5.0/Classes/classes.jar
(21MB, and that includes something like seven independent
implementations of the DOM.) ocaml-3.09.2.dmg is only 13MB; maybe
there's an unmet need for an "OCaml distribution" that includes
standard libraries that cover most of these things and weighs in at
25-50MB.
- a more sophisticated unit-test framework than "assert"
- basic algorithmic stuff:
- priority queues (there's one on p.25 of the user's manual but none in
/usr/local/lib/ocaml)
- binary search
- auto-resizing arrays (like OrderedCollection or Python's lists; the
Buffer library has an implementation for strings)
- CRC
- random-shuffle and random-choice
- parsing and production of common data formats:
- SGML or HTML or XML
- any parser at all would be a big improvement, but if it had some of the
horrible standard interfaces like DOM, SAX, XPath, or even XSLT, I could
pick it up and use it more easily. Amusingly, one of the best XQuery
implementations is written in OCaml.
- URLs
- CSV
- RFC-822 parsing
- MIME
- RSS
- mbox
- UTF-8 (!)
- other character encodings
- .ZIP files
- networking things:
- a socket interface that lets me open a TCP connection to a DNS name
with a single function call
- an HTTP client library (server would be nice too, but its absence is
less surprising; but, hey, guys, I hear this WWW thing might catch on)
- other things are also missing but that's less surprising (SSL/TLS, IMAP,
FTP, SMTP, SNMP, LDAP, SOAP, XML-RPC, IRC, NNTP, POP)
- signal-processing things:
- the ability to read and write common image formats (JPEG, PNG, GIF, PBM)
- the ability to lines, text, and other graphical objects on them, with
alpha and antialiasing
- the ability to read and write common audio formats (AU, AIFF)
- other DSP stuff would be gravy but it's not surprising it's missing; the
Bigarray library contains roughly the absolute minimum
- I'd complain about the absence of GUI stuff, but actually I think
it's included, but doesn't work due to the absence of C development
tools on this Mac. (Although maybe it only works on X11.)
- type-safe marshalling and unmarshalling, at least for some subset of data
types (e.g. JSON, YAML)
- some kind of RPC (I'm not asking for IIOP here!)
- data compression and decompression tools
- SHA-1 and other cryptographic tools (it's nice that it has MD5, but DES, AES,
and RSA would be big pluses)
- termcap
- some kind of DBM or Berkeley DB interface
- some kind of SQL interface (at least SQLite!)
- some kind of date handling (although the Unix module does have
gmtime, localtime, and mktime, it doesn't have strftime, and using
mktime to print a calendar is kind of a pain)
- file pathname manipulation (dirname, basename, relative-path resolution)
- some kind of concurrency handling. I'm not saying I want threads
--- I would never say that --- but threads, or Erlang-style
processes with easy communication, or Twisted-style reactors, or
something! Maybe there's something in the "vmthreads" directory,
but it's hard to tell (see sections Missing Documentation and
Insufficient Introspection for why.)
- some kind of logging for debugging (more than just Printf.fprintf stderr)
A lot of these things, maybe all of them, are available in other
libraries you can download from various sites on the internet.
Missing Documentation
---------------------
The MacOS X man pages for things like "ocaml" suggest reading "The
Objective Caml user's manual", but it does not seem to be included in
the disk image from which I installed OCaml. Also, many of the .cm*
files in /usr/local/lib/ocaml lack man pages.
There are occasional errors in the documentation (for example, the
Pervasives man page, for example, says that integers are 31 bits wide
rather than 30; and the manual uses {< >} in an example a few sections
before explaining what it is) but I don't think they're as important
as the documentation that's missing altogether.
Later, when I had internet access again, I downloaded "The Objective
Caml system release 3.09 Documentation and user's manual", which I
think is the user's manual referred to in the man pages; but I find
that it refers me to the Camlp4 documentation, which I'm pretty sure I
don't have, for information about pattern-matching on streams.
Unfortunately I may not have internet access again for a while.
Arbitrary Limits
----------------
These limits seem most unfortunate to me:
# Sys.max_string_length ;;
- : int = 16777211
# Sys.max_array_length ;;
- : int = 4194303
I frequently deal with chunks of binary data bigger than 16 megabytes
in size, and arrays of more than 4 million items; I'm typing this on a
laptop with two gigabytes of RAM.
And the standard file operations (seek_in, pos_in, etc.) use 30-bit
numbers to represent file positions and sizes, with the consequence
that every use of them adds a bug to your program whenever it deals
with a file over 256MB in size. (Unless you're on a 64-bit platform,
I think.) The problem with this is that 256MB is now 15 cents' worth
of disk space, so I have a lot of files bigger than that.
Too Much Early Binding
----------------------
I'm sure Xavier Leroy and company, too, wish seek_in and pos_in used
64-bit ints, and that you could tell what key was Not_found in an
association list, and that you could change your program from storing
stuff in an array to storing it in a hash-table easily when the keys
turned out to be sparser than you expected. The module system makes
the last item at least possible, but the others are not supported in a
backwards-compatible way by the type system.
Of course, a great deal of the Zen of OCaml is that lots of binding
can happen at compile-time rather than at run-time, so your code can
get type-checked and maybe run fast too. But the binding I'm
complaining about is happening earlier, at edit-time; you have to
actually change your source code in order to accommodate the use of
Unix.LargeFile.lseek and its int64 arguments. It isn't obvious to me
that, in principle, static typing requires these sorts of decisions to
be accidentally strewn across your entire codebase, given the
existence of type inference and parametric polymorphism in the form of
functors.
This is not a new problem --- it happens in C programs all the time,
due to the ubiquitous explicit static typing. It's just that I had
the impression that OCaml had perhaps conquered this problem, as have
the languages I normally use day-to-day.
Here's another, smaller-scale example. My unit tests largely consist
of code like this:
assert ([45; 50] = eval (Atom [45; 50]));
Maybe at some point I would like to use floating-point for all my
interpreter's values instead of integers. I could imagine a language
where I could retain compatibility with these tests by making Atom
into a function that does the necessary conversion for the old tests;
but that language is not OCaml, because in OCaml, your functions
cannot begin with capital letters.
Clumsiness Working With Source Code Files
-----------------------------------------
The unfortunate necessary duplication of code between .ml and .mli
files (if you have .mli files) is well-documented elsewhere. But how
about if you want to interactively test some code from your module to
see why it fails a unit test? Here's how I load that code into a
toplevel so I can test it:
Beauty:~/devel/ocaml-tut kragen$ ocaml toyapl.ml
Exception: Assert_failure ("toyapl.ml", 62, 4).
Beauty:~/devel/ocaml-tut kragen$ ocaml
Objective Caml version 3.09.2
# Toyapl.eval ;;
Characters 0-11:
Toyapl.eval ;;
^^^^^^^^^^^
Unbound value Toyapl.eval
#
Beauty:~/devel/ocaml-tut kragen$ ocamlc -g -c toyapl.ml
Beauty:~/devel/ocaml-tut kragen$ ls toyapl.*
toyapl.cmi toyapl.cmo toyapl.ml toyapl.ml~
Beauty:~/devel/ocaml-tut kragen$ ocaml toyapl.cmo
Exception: Assert_failure ("toyapl.ml", 62, 4).
Beauty:~/devel/ocaml-tut kragen$ # I edit the file, comment out the assert
Beauty:~/devel/ocaml-tut kragen$ ocamlc -g -c toyapl.ml
Beauty:~/devel/ocaml-tut kragen$ ocaml toyapl.cmo
Objective Caml version 3.09.2
# Toyapl.eval ;;
- : Toyapl.toyaplexpr -> Toyapl.toyaplval =
The (faked) equivalent in Python:
>>> import toyapl
Traceback (most recent call last):
File "toyapl.py", line 62, in test
AssertionError: ([45], 45)
>>> toyapl.eval
Python checks to see if there was a bytecode file for toyapl.py, and
if so whether it is up-to-date; if not, it recompiles it; then it
imports the bytecode file. There is an exception during import (my
unit tests normally run during import) but because I'm in an
interactive toplevel, the module remains imported anyway so I can poke
at it and see what was wrong.
In a similar vein, consider this.
Beauty:ocaml kragen$ cat > time.ml
print_float (Unix.gettimeofday()) ;; print_string "\n"
Beauty:ocaml kragen$ ocaml time.ml
Reference to undefined global `Unix'
Beauty:ocaml kragen$ (echo '#load "unix.cma" ;;'; cat time.ml) > time2.ml
Beauty:ocaml kragen$ ocaml time2.ml
1170635455.04
Beauty:ocaml kragen$ ocamlc time2.ml
File "time2.ml", line 1, characters 0-1:
Syntax error
The #load directive makes it work in the interpreter, but causes a
syntax error in the compiler. I think the right solution is to run
the program with "ocaml unix.cma time.ml" and compile it with "ocamlc
unix.cma time.ml", but it's a non-obvious failure mode.
This particular problem (in effect, the interpreter supports a
superset of the language supported by the compiler, and I made the
mistake of using one of the extensions) could be ameliorated by a more
helpful error message.
Syntax
------
I'm not going to belabor the point on syntax. Everybody knows there's
a problem; I explained some aspects of the problem in some previous
sections. This bites newbies like me particularly hard.
Out-Of-Dateness
---------------
ocamlopt generates PowerPC assembly code on this Intel MacBook, but
it's January 2007. See also the complaints about libraries.
Possible Improvements
=====================
Probably some of these are bad ideas, as will be obvious to me after I
have more experience. I'm not (yet) volunteering to do them.
1. A fatter distribution. Including the manual (1MB), ocamlnet
(800K), and the ocamlnet manual (450K) would go a long way toward
removing my own biggest frustrations.
2. A simpler syntax with fewer levels of precedence. Maybe writing
OCaml in S-expressions would be going too far, or maybe not.
Clearly this would make it effectively a different language.
3. No missing man pages.
4. No arbitrary limits on object size (other than physical address space,
of course.)
5. Clearer wording for many error messages.
6. Reflection/introspection capabilities including the ability to
interactively list the contents of modules. Full reflection could
be difficult to reconcile with static type-checking, but I think
it's possible; but even modest reflection capabilities would help a
lot with interactive use.
7. Self-documentation --- the ability to attach doc strings to
functions and types, and doc strings being present throughout the
standard library.
8. Exceptions that accumulate a full stack trace. This is a little
hairy to get to work in native code, but I don't think it's
intractable.
From kragen at pobox.com Mon Feb 26 03:37:01 2007
From: kragen at pobox.com (Kragen Javier Sitaker)
Date: Mon Feb 26 03:37:02 2007
Subject: the presentation of "private" in programming languages
Message-ID: <20070205201712.B72E6E34138@panacea.canonical.org>
C, C++, OCaml, Eiffel, Ada, most assemblers, and the like have
facilities for "mandatory information hiding", in which some parts of
the program are not accessible to other parts of the program.
Textbooks and academic papers on such things often describe their
benefits in terms of security, with examples like bank-account objects
that must conserve funds in the face of malicious client code.
It's often useful to think about software robustness properties in
terms of robustness against malicious attacks, since if a malicious
attacker wouldn't be able to violate some robustness property, then no
neither can client code that is simply malfunctioning. The benefit of
thinking in these terms is that it avoids the necessity to model the
possible malfunctions of the client code and their probabilities, a
cognitive task generally so difficult as to be infeasible a priori.
And there have been various systems that use language-level
information-hiding facilities to enforce security properties --- E is
the best-developed example, but Java has tried to do this as well.
However, though, it is not necessary to resort to arguments about
robustness in the face of partial failure or malicious attack to
justify the introduction of these mechanisms. The benefit they have
given me from time to time is quite different.
It's well known that most of the time spent on programming is spent on
"maintenance", by which we mean modifying software somebody is already
using, usually to add new features; and that most of the time spent on
"maintenance" is spent understanding the existing code so as to be
able to make modifications that won't break other things and won't be
hard to understand again later. Much of this activity consists of
understanding what parts of the code must be changed together, and how
they must be changed to ensure that the code continues working.
The advantage of mandatory information hiding is that this task
becomes simpler. If I'm changing the semantics of a private method in
C++, I need only look within that method's class for calls to the
method; I don't need to look anywhere else.
I have occasionally wished for this feature in Perl on a 100 000-line
project.
This understanding of facilities for mandatory information hiding
suggests some differences over the standard understanding:
- The facility is still important even when you don't have malicious
code running in your process space, and even in languages like C++
or Python where there is no really effective access control.
- The point of the facility is not to keep information from your team
members, although indeed some of its earliest proponents thought it
was. Therefore, changing a method from private to public because
you need access to it from somewhere else is not a sin. (At least,
not for this reason.)