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.)