; In arc.arc, or maybe something equivalent in
; ac.scm.
(typeclass 'cons 'car 'cdr 'scar 'scdr 'apply)
; In our library, where we can't (or at least,
; "shouldn't") redefine the built-in Arc 'cons
; typeclass.
; A perfect subset of 'cons.
(typeclass 'scanner 'car 'cdr)
; Dumps out a character scanner or list
; to a port
(def dump-out (port l)
; Note: we expect has-a to return t
; even if l is a 'cons - i.e. the type
; system should be smart enough to
; realize that 'cons provides all the
; interfaces of 'scanner. Prolly something
; like:
; (= all-functions (mappend [instance-functions-of _] (type-classes-of ob)))
; (all
; [some _ all-functions]
; (instance-functions-of requested-type-class))
; above can be optimized by using tables - put all
; of the requested type class's instance functions
; in the table, then remove everything that exists in
; all-functions; if we finish all-functions while
; table entries exist, we know the object doesn't
; have that type class
(unless (has-a l 'scanner)
(err "Not a scanner"))
(map [write _ port] l))
Now, the thing I like about implicit has-a relationships is that they guarantee less code than making it explicit, in that they're the same amount of code sans type declarations and checks. For example, the above would be simply
(def dump-out (port l)
(map [wrie _ port] l))
No typeclass declarations and no type checking. This also has the advantage of being identical to the code you'd write if you were completely unaware of a scanner abstraction.
Also, on a more philosophical level, I feel like type checking is just out of place in a dynamically-typed language like Arc.
...which ends up being the optimizing compiler's clue:
(def dump-out (port l)
; a completely optional declaration
; for:
; (1) an non-optimizing interpreter, so that
; you have some chance of checking errors, and
; (2) the optimizing compiler, so that it can
; do some shortcuts.
(type-declaration l 'scanner)
(map [write _ port] l))
^^
You can then trivially redefine type-declaration as:
(= type-declaration
(annotate 'mac nilfn))
To disable type-checking.
Oh and yeah: type checking is plenty in-place, if you're building a library. If the library's interface functions simply passed the object to library-internal code without checking, the error will pop up as coming from some code deep in your library, where your user might be less inclined to trace (because it might be your bug, not theirs). At least if you do the check on the interface functions themselves you have proof that the bug is in the user's code.
But that macro, and the whole plumbing behind declaring and checking type classes, just don't have to exist if you make it implicit. It's a lot of added complexity for no gain in power. All you gain is a little type safety - and if you need type safety, Arc (and Lisp in general) is not your language.
I wonder how much type-checking major Lisp libraries really do have. In the dynamically-typed-language libraries I've looked at, there are more or less no type checks. In the Ruby community, they're specifically discouraged, because they limit the power of duck typing.
Yes, but who says it has to be used? In the majority of code, there won't be type checking, but we do want to say something like this:
This function accepts a list of ordinal types
which we can simply put in-code as:
(type-declaration a 'Scanner)
(type-declaration (car a) 'Ordinal)
The point is that the function doesn't specifically accept a list of strings or numbers - it accepts a traversable sequence of anything that can be ordered. The type-declaration thing is just a notation to express that to someone else.
[1] This is about correct; Haskell dispatches off the is-a ness of the type. However, code you write will generally ask for the has-a relationship:
{- merge used for mergesort -}
merge :: Ord a => [a] -> [a] -> [a]
{- The "Ord a" above asserts that the type "a"
should be an "Ord"inal, i.e. it should support
< <= > >= == methods
-}
merge [] [] = []
merge a [] = a
merge [] b = b
merge a:as b:bs
| a < b = a:(merge as b:bs)
| Otherwise = b:(merge a:as bs)
{- Of course, I haven't programmed Haskell in a while,
so the above code might be wrong. -}
If we had has-a relationships, we might say something like this in Arc:
(def merge (a b)
(unless (and (has-a (car a) 'Ord)
(has-a (car b) 'Ord)
(is (type a) (type b)) )
(err "Type error, needs Ord"))
(if
(no a)
b
(no b)
a
; now we're assured < works
(< (car a) (car b))
(cons (car a) (merge (cdr a) b))
(cons (car b) (merge a (cdr b))) ))
Hmm. LOL. I can just imagine a macro to do that type checking for you:
(type-check (list a b)
(Ord a) ((a . as) (a . as)))
It's beginning to look lot like Haskell ^^. Heck. arc.arc is congruous to Haskell.Prelude
Yeah, I've seen several ways to express has-a relationships. Statically typed languages like Haskell tend to express them as is-a relationships (e.g. Int is-a Ord), since that meshes well with type safety. Languages that embrace duck typing, like Ruby, tend to make has-a relationships implicit; just use the method (or in Arc's case, function) and assume that the receiver will react properly. This meshes well with the late-binding, screw-static-verification philosophies of these languages, which Arc generally shares, and the message-passing object model, which Arc does not.
This is where I assume Arc will end up building its type system, if it ever does embrace a single type philosophy[1]. Even if it doesn't have a message-passing model, its dynamism makes the implicit has-a model a good fit.
The only language I've seen that embraces explicit checks for has-a relationships, as opposed to expressing them as is-a or making them implicit, is Javascript. This is mostly because you're dealing with objects that have an inherently variable and ill-defined interface that you don't know a lot about at compile-time. I think most JS developers view it as an annoying necessity, though, so I'm inclined to believe it's the least pleasant way of dealing with has-a (despite being the most explicit).
[1] Speaking of which, I really think it should. This is sort of implicit in taking part in all these discussions about typing, I suppose, but I wanted to mention it anyway. Any sufficiently powerful, has-a-based type system won't be a constraint on the language, but will rather allow powerful abstractions like Ruby's #each and almkglor's scanner.
"Abstract Base Class" also happens to be the term preferred in C++ for Haskell's "type class" and Java's "interface". So it probably is-a python abstract base class ^^.
That said the problem with using a "base" class with an inheritance model is that it's deucedly difficult to think in terms of multiple inheritance (multiple inheritance is so hard most languages don't use it). Mix-ins help ameliorate this by being a has-a relationship.
Yeah, sorry about that. I wasn't thinking about performance when I originally put infix.arc up on Anarki, and since I was doing a lot of testing at the time I found it convenient to add it to libs.arc. Perhaps it would be best to leave it out by default though.
On the git. ^^ It's also somewhat optimizing: the representation is mutated if 'car/'cdr is invoked on the scanner, and the function that captures the expression's environment is replaced with a function which returns the expression's result. Of course, I made sure not to actually measure any so-called improvement in time. ^^
(translation, in case it's just someone who clicked the wrong country:
dido: Who's the third?
almkglor: Hmm! It is three! Maybe someone just clicked wrong?)