This post is a contribution to the discussion of type systems that almkglor kicked off a few days ago (http://arclanguage.org/item?id=4855). It's been a really interesting discussion and has raised all kinds of genuine criticisms about the way Arc handles things, and a number of proposed solutions. This is my proposed solution to the problem, and it's quite different (and yet similar) to everyone else's. My post is rather long, because I want to give all the reasons why I did things the way I did, so apologies for that. Please let me know what you think. Before I go any further, let me say that my feelings on typing are very simple. (> fewer-types more-types)
=> t
I like the functions-per-type ratio to be high, so I avoid creating new types when possible. In fact, I haven't found the need to create a single new type in my Arc programming so far.But the problem will arise eventually, and when it does we'll want our old functions to work with the new types. Arc is already pretty overloaded (e.g. functions that work on strings and lists of strings) so we can assume it'll get even more overloaded as new types turn up. pg has already given a way to make an old function work with new types. Just redefine the function to accept the new type. We even have a macro in Anarki to make this easier. This problem is solved. The real problem arises further upstream, when you have functions which call these redefined functions, but switch on the value of isa or islist and things like that. These functions will fail because they look at the intrinsic type of your object, and are hard-coded to only accept a few types. Other than redefining isa to work in mysterious ways, there's no easy way to solve this, so functions that act like this need to be rewritten/redefined. What we need is a standard way of rewriting them that ensures functions always call the right function on the right type, without knowing ahead of time exactly what types may be passed to it. This is the essence of polymorphism. My solution has the following benefits. Firstly, it allows you to tell a function what capabilities your type has (e.g. is it a sequence? Does it have a car?). This is what I think everyone means by 'has-a' semantics. You can do this two ways: you can either say what functions your type supports, or you can say that your type implements an abstract type (interface) and therefore supports all the operations on that abstract type. I think being able to do both is best, and you can do both with my solution. From now on though, I'll assume we're doing the first one because it makes the code simpler. Secondly, it allows you give your type different capabilities in different situations. This is useful because you may have a type that can be treated, for example, as an array or a sequence, but you want to force a function to treat is as a sequence because you don't like what it does to arrays. Thirdly, it allows you to implement all kinds of crazy inheritance systems, object-oriented DSLs and stuff like that, if that's the kind of thing that floats your boat. In fact, the whole solution is designed to be super-flexible, so you can implement all kinds of type systems on top of it. It's not so much a type system, as a way of dynamically extending your type system to fit your requirements. It's like the Meta-Object Protocol, only simpler. Fourthly, it doesn't redefine functions like type and isa, nor does it interfere with any other type system you may want to implement. Caveat: you still need to redefine other core functions if you want to make them polymorphic. Fifthly, it does all this with just one type! That's the bit that keeps me happy :) So how does it work? You simply create a new type called a polymorph (the Red Dwarf reference should give you a clue as to what it does): (def polymorph (value methods)
(annotate 'polymorph (cons value methods)))
Then you write your polymorphic function like this: (def polymorphic-car (x)
(call car x)))
The call macro has the following behaviour: if x is not a polymorph, then simply call the supplied function on the object. (car x)
But if x is a polymorph, look up the required function in the methods part of the polymorph object, and call it on the value part. The methods could be stored as a hash table or similar. (((cdr x) 'car) (car x)) ;; [Note: need to fix multiple evaluation of x.]
So the methods part stores all the methods for that object. If you want to use the second kind of polymorphism, where you specify an interface instead of a particular function, then you just store the interface in the hash, and this interface object itself contains all the functions. ((((cdr x) 'sequence) 'car) (car x))
So two polymorphs are of the same 'type' if they use the same methods object.There's lots of flexibility here already. For example, every polymorph can be given different methods. You're not restricted to having every value of a given type use the same methods, but can set things up however you want. Also, you can always fish the value out of your polymorph and stick it in a new polymorph with different methods. That's how you make the behaviour of your type relative to a given context. But it can be made even more flexible. So far, I've assumed that the methods are stored in a hash list. But suppose that we used a polymorph instead, and rewrite call so that it is also polymorphic. If the methods are a hash, then do as above, but if they're a polymorph... ((call (getmethod (cdr x)) 'car) (car x))
What this means is that we can now use more interesting objects (anything that implements getmethod) to store the methods. Examples:1. If we want our types to have single inheritance, then we could create a polymorph that contains a list of hashes and has a gethmethod method that traverses them to find the right method. 2. If we want multiple inheritance, then we could use a tree of hashes instead of a list. 3. We could have a different hash for each interface. Then we could add/drop interfaces as required. 4. We could even use an object that generates methods on the fly by returning closures. Ruby on Rails abuses that kind of feature a lot. I'm sure we can find a use for it in Arc too. You can even use all of these side-by-side. They don't interfere with each other, because each system is encapsulated in a different kind of polymorph. You can even compose them together. For example, instead of implementing single inheritance with a list of hashes, you could use a list of polymorphs, and so on. We could go even further: I stored the method and value of the polymorph as a dotted pair, but there's no reason why this can't be a polymorph too. This would allow you to store extra information in your polymorph, such as a list of interfaces it supports. You can even make call even more polymorphic by having call methods in your polymorphs, so they can decide how to call themselves! This could be used for implementing multiple dispatch and who knows what else. A fully polymorphic type system like this gives you a lot of the power of the Meta-Object Protocol, but in just a few lines of code. |