So, after much thinking, here's the current state of Nulan's type system. Objects are suuuuuper-important in Nulan, much more so than most Lisps which just use cons for everything[1]. As I've explained in the past, everything in Nulan is immutable[2], so to "change" an object, you create a new object which is just like the old one but with the changes applied. But what is an object, exactly? Well, I decided to go for an extremely simple model. An object is simply an ordered binary search tree with keys and values. In other words, a dictionary. This makes Nulan's objects basically the same as objects in JS or Lua, except that they're immutable, sorted, and don't have inheritance. Now here's where types come in. A type is basically just a gensym, an atomic unique identifier. You can take this type and use it as a key in a dictionary. Rather than thinking about is-a relationships (is it a number? a string? a duck?) you think about has-a relationships: does this object possess this type as a key? Here's an example: $def %foo (type)
$def foo
(%foo F) -> (F 10)
Okay, we've defined the variable "%foo" to be a new anonymous type, which is just a gensym. Now we define a "foo" function which pattern matches an object that has a "%foo" key and binds the variable "F" to the value. We then call "F" with the number 10.How do you create new "instances" of the "%foo" type? Well, you can create a new object using the {} syntax, just like in JS[3]: $def foo-instance { %foo (X -> X + 20) }
This created a binary search tree with the type "%foo" as its key, and the function (X -> X + 20) as its value. It then assigned that tree to the variable "foo-instance"Now let's try passing it to the "foo" function: (foo foo-instance)
The above will return 30. What happened is, the "foo" function destructured the object, looking for a "%foo" key. It found it, and bound the value to the variable "F". It then called "F" with 10, and "F" is the function (X -> X + 20) which thus returned 30.And that's it. That's the entire type system. There's no inheritance, there's no mucking about with things, it's just simple flat objects with gensyms as keys. Rather than thinking about types as being in some sort of hierarchy, you think about types as being attributes: does this object have this type? You can add, remove, and change types on the fly, which works out very well because objects are immutable. --- That's great and all, but I ran into a slight snag. I wanted to have only two primitive things in Nulan: types and binary search trees. Types are atomic gensyms, and binary search trees are a combination of type/value pairs. But when I got around to implementing things like symbols, strings, numbers, etc. it became a bit complicated. These things need to be implemented in terms of JavaScript strings and numbers, and they need at least the "%lt" and "%is" types. The problem is, how to store the data? What I mean is, let's say you have two numbers. You need to implement a %lt method for them, but how does the left side know about the right side? In other words, how do I take this binary search tree and somehow extract the raw JavaScript number from it? I can't store it as a JavaScript property because it'll be shadowed when the tree is changed, so it has to be stored inside the tree. Hm... I guess I could use a "&%js" type, which will convert a Nulan object to a JS object. That would be nice, but it does complicate things a lot... The other option is to simply make strings, numbers, and symbols primitive. I'm actually really leaning toward this, but I'm not sure. The primary downside is that you can't just take a random string/number/symbol and slap arbitrary properties on it: they're atomic, just like types. --- * [1]: Nulan doesn't have cons at all. Everything is binary search trees, which means "lists" are almost exactly like JavaScript arrays: they're just objects which have numeric keys, and a special "%len" type. * [2]: The sole exception to this is variables, which are simply a single-celled box used to hold a value. * [3]: You can also update existing objects in two ways: set foo-instance %foo ...
{ @foo-instance
%foo ... }
The first way uses a function called "set" to update "foo-instance", whereas the second way uses the splicing form @. This has a whole slew of benefits... it makes it easy to merge objects together: { @foo @bar @qux }
It makes it easy to update an existing object with multiple properties: { @foo
%bar ...
%qux ...
%corge ... }
It makes it easy to define the order in which things are merged: { %bar ...
@foo }
The above will create an object that uses "foo"s definition of "%bar", but if it doesn't exist, it'll use "...". In other words, the above provides a default implementation of "%bar" if it doesn't already exist in "foo". |