Librarian of Alexandria

2012-08-25

Rubbish and Blatherskite

I'm working on a programming language called Rubbish. It is called Rubbish because it is rubbish. Nobody should use it. I will not use it beyond its simple purpose, which is a personal thought experiment.

If this appeals do you, then learn Lisp instead. All dynamic languages are Lisp with a handful of macros and a modicum of discipline, anyway.

In practice, the Rubbish language will have a handful of built-in types—an integer type, a floating point type, a string or rope or something—but, for our purposes, let's assume it has only two types: variants and functions. These two are the only two currently accounted for in the semantics, which I will type up at some point.

Rubbish is weakly typed, so only values have an associated type; variables can take on any type. The primary data type mechanism is the variant, which are analogous to the tagged sums of strongly typed functional languages except without type guarantees. A variant has an obligatory name, which begins either with a capital letter (e.g. Foo), and an optional tuple of expressions, which are evaluated in sequence and stored along with the tag name. Variants are immutable, so there is no mechanism for changing the tag, the values stored with the tag, or the number of values. (If the values themselves are mutable, then they may of course be changed.)

The only meaningful operations on variants are equality and pattern matching; equality may be seen as a deficient form of pattern-matching. Pattern-matching is the only way of extracting information from a variant. For example:

rsh> my_point := Foo(One, Two)
rsh> my_point = Foo
False
rsh> my_point = Foo(Two, One)
False
rsh> my_point = Foo(One, Two)
True
rsh> match my_point { Foo -> Nullary
                      Foo(x) -> Unary(x)
                      Foo(x, y) -> Binary(y, x)
                      Foo(x, y, z) -> Ternary(z, y, x)
                      _ -> Other
                    }
Binary(Two, One)

We can use them as a cheap and easy way to define the Peano numbers.

rsh> add := fun(x, y):
       match x {
         Zero -> y
         Succ(x') -> add(x', Succ(y))
       }

I plan on eventually implementing user-definable mix-fix operators, so you could eventually write the following:

rsh> _+_ := add
rsh> Succ(Succ(Zero)) + Succ(Succ(Succ(Zero)))
Succ(Succ(Succ(Succ(Succ(Zero)))))

The other type is functions, which are introduced with the fun keyword and take a tuple of arguments. There is another syntax for functions of one argument, and it looks kind of like a hash literal. The definitions of isZero and isZero' below have identical functionality. (Notice that this means match x cases is basically syntactic sugar for cases(x).)

rsh> isZero  := fun(x) ->
       match x {
         Zero -> True
         _    -> False
       }
rsh> isZero' := { Zero -> True
                 _    -> False
               }
rsh> isZero'(Succ(Zero))
False
rsh> isZero'(True)
True

Combine this with the following sprinkling of syntactic sugar:

_._ := fun(f, x): f(x)

And we get the following:

rsh> makePoint := fun(x, y):
       { IsOrigin -> x = 0 and y = 0
         GetX -> x
         GetY -> y
         Add(p) -> makePoint(x + p.GetX, y + p.GetY)
         ToString -> format("Point({0}, {1})", x, y)
         method -> raise MethodNotFound(method)
       }
rsh> a := makePoint(2, 3)
rsh> b := makePoint(4, 5)
rsh> a(IsOrigin)
False
rsh> a.IsOrigin
False
rsh> a.ToString
"Point(2, 3)"
rsh> a.Add(b).ToString
"Point(6, 8)"
rsh> a.Add(makePoint(-2,-3)).IsOrigin
True

While variants can encode data, functions like these can easily encode codata like streams.

rsh> ones := { Head -> 1
               Tail -> nats
             }
rsh> streamMap := fun(f, stream): { Head -> f(stream.Head)
                                    Tail -> streamMap(f, stream.Tail)
                                  }
rsh> nats := { Head -> 1
               Tail -> streamMap(fun(x): x + 1, nats)
             }
rsh> take := fun(num, stream):
       match num {
         0 -> Nl
         _ -> Cs(stream.Head, take(num - 1, stream.Tail))
       }
rsh> take(5, ones)
Cs(1, Cs(1, Cs(1, Cs(1, Cs(1, Nl)))))
rsh> take(5, nats)
Cs(1, Cs(1, Cs(3, Cs(4, Cs(5, Nl)))))

Finally, because of mixfix operator definition, we are free to do some pretty horrific things. I am omitting precedence declaration here because I haven't decided on a syntax for it yet.

rsh> _->_ := _._
rsh> nats->Tail->Head
2
rsh> _<-_ := fun(x, f): f(x)
rsh> Head<-Tail<-nats
2

That's some Rubbish for you. There's one major feature I haven't covered yet, and that's delimited continuations. As I said before, if any of this appeals to you, then you are a horrible human being, but I would recommend a healthy dose of Scheme, ML, or Haskell.