You're receiving quite a lot of flack for this. Personally, I think it's a great idea. I'd like to see Arc running on as many environments as possible.
I appreciate everyone's comments, both the support and the criticism. And I'd like people's help with something.
At the moment I have one major problem: LispNYC does not deem CL-Arc to a be a full summer's worth of work. So unless I can show them that CL-Arc really is a summer's worth of work, I'm left with either coming up with additional ideas which I can work on after CL-Arc, or writing an entirely new proposal on another topic.
I'm wondering if anyone has ideas for what I could work on after completing the basic CL-Arc. If the axioms and core language take a couple of weeks, and the libraries take a couple of weeks, and FFI (probably just a port of the current FFI on Anarki) takes two weeks, that leaves a lot of time in the summer. What can I do with this time?
One idea is I could port Arco to CL-Arc. Unfortunately Arco isn't complete and is undergoing rapid changes, and might be completely different by the time I get around to CL-Arc. This makes it even more difficult to write a definite proposal.
> How long do you think defcall and settable-fn would take?
About a week, maybe less ^^. Defcall is reasonably trivial although it requires some rethinking. settable-fn is implementable using defcall (see nex-3's settable-fn2) but I'm personally dubious of such a style, and prefer my own.
> (But I'm a little dubious about working on something pg may have already done.)
We don't have evidence either way - pg hasn't commented on this (none that I've seen, anyway). Up to you to decide whether to act as if pg made it, or act as if pg didn't make it.
You could add to the proposal various useful libraries, such as libraries to use HTTP, FTP, SMTP, etc. protocols, bindings to access databases, graphic libraries, etc. You just have to choose.
I'd be interested in knowing what you found simple/difficult in working with Arc. There's precious few applications written so far, so it would be nice to know where you found the language lacking.
Good question. Here are a few things that tripped me up:
1) An easy way to call differing functions based on the data type of an argument (polymorphism). Ruby has a nice approach as shown here (see the section marked 'Duck Typing'): http://www.ibm.com/developerworks/java/library/j-ruby/
2) I spent a while chasing a bug in an 'if' statement because I neglected to surround multiple statements with a 'do' statement.
3) It wasn't obvious to me how to build a bitmap array. I naively created an array filled with zeros for each scanline. I would then push this scanline onto another var. But it turned out all of the scanlines pushed this way were pointing to the same scanline. This was a problem until I found out how to copy a var in Arc (use copy!).
4) It's not clear to me how you modularize code, so everything is in one file.
5) Coding style. I have none when it comes to Arc, and there are few examples. For example, I didn't realize that an Arc convention is to append an asterisk to global variables (This follows the Lisp convention I think).
6) Spent time figuring out how to make my vector operations take list arguments without quotes (This was my initiation into the world of macros). Then realized that this was not necessary since these functions would have variables passed in arguments, not literal lists.
7) Errors were not specific (understatement).
8) No raw file output, or low level bit operators. So I added these to the base language.
1) Hmm. Currently I've been experimenting around with Arc's type system. The type system is "not bad" but there's a definite problem: making a user-defined type quack like a built-in type is very difficult. My "Create your own collection" series is probably of interest in this research.
If however you are using only user types (not masquerading existing types), then nex3's defm macro is your friend: http://arclanguage.com/item?id=4644
2) LOL. However, if you need just the "then" branch, or just the "else" branch, you can use the 'when and 'unless macros:
(from "arc.arc")
[mac] (when test . body)
When `test' is true, do `body'.
See also [[unless]] [[if]] [[awhen]]
nil
arc> (help unless)
(from "arc.arc")
[mac] (unless test . body)
When `test' is not true, do `body'.
See also [[when]] [[if]] [[no]]
nil
4) Definite problem. In fact, pg's own code doesn't seem very modularized: bits and pieces of the various parts of the webserver are in srv.arc, html.arc, and app.arc. Fortunately Arkani's 'help command also includes which file the function is defined in, so at least you can track files a little (although in practice I often find myself looking for definitions using grep).
5) Arkani has a "CONVENTIONS" file for some conventions. However it's incomplete IMO, so probably we need to fill this in somewhat.
7) True, true. Sure, Arc code is fun to write, until it encounters a problem. Then it's hard to debug/optimize/whatever
I had the same problem on Windows and I fixed it by moving to Anarki stable. I don't know which patch fixed it though, so I don't know what's causing it.
It doesn't do multiple dispatch, so you can't really do all of this yet. Multiple dispatch could either be implemented in call or by putting the call implementation in the polymorph.
If you put it in the polymorph then you have to reimplement it each time, which is bad. On the other hand, you can customise how multiple dispatch works for different types. You'd probably need that for arithmetic, because you always have to coerce values to the most general type (e.g. integers to rationals). I don't know any other types that do that. It's a special case.
Vector types could perhaps be implemented by supporting the interface '(vector n) where n is the dimension. Then your complex numbers could implement '(vector 3) or something. Dependent typing is not something I've thought about. Maybe it can be done.
Another way of doing this could be to have alternatives to call. You could have a simple single-dispatch call, then another (multi-call) that works like multiple-dispatch in CLOS (using inheritance hierarchies to choose a method), then another one that coerces everything to the 'most general type' (whatever that means in the context).
"I don't know any other types that do that. It's a special case."
Suppose I'm modelling a starfield for a computer game, and I have ships and missiles flying around. I then, quite reasonably enough, have a (collide a b) function that I call whenever I notice that one of the ships collides with a something else. We could have ships colliding with ships, or colliding with missiles, or missiles colliding wiht other missiles. Worse, we've relegated our collision detecting code to a loop that goes through the list of game elements, so we can't exactly say that a will be a ship or a missile (or even an asteroid). Different things will happen based on the type of object - missiles just cancel each other out, but ships have to explode convincingly, and of course there's a special handling for the player's own ship (because it's game over then).
It's not a web app, but hey ^^
So multiple dispatch is in fact a pretty good strength in CLOS, and it's generally done over the is-a-ness of the object ^^.
Somehow I feel that having two layers - an is-a type attached to an object, and has-a interface semantics attached to functions on an object, might work. Basically a type declares a set of has-a interfaces, and provides (using multiple dispatch) the functions used to implement the interface. So an object is-a ship and is-a missile and is-a asteroid and is-a player-ship, and each of those types has-a collideable interface, defining how different objects react to being bashed against one another.
(1) Yep, I agree. You'd want to put the polymorphisn all the way down, so you nearly never have to actually use call in your code.
(2) Again, agreed. Implement features on top of the system, rather than within it, just like Arc!
(4) Might be tough to make everything completely implicit. Ideally, the whole thing should be invisible, like it didn't even exist, but works anyway. All suggestions for doing that are welcome.
(4) Actually, I think we're in agreement there, I just didn't say what I meant too well :) In fact, one of the reasons your post may look so explicit is that you're discussing how to implement the system, and thus have to deal with a lot of "low-level" code, the way the first few lines of arc.arc look nothing like most Arc you'll actually use (safeset, etc.). For instance, call certainly appears to be such a feature—used to get the system working (and possibly sometimes used the way Ruby uses its #send method), but not in every case. Similarly, since you're discussing implementing the system, of course implementation details will show through. At any rate, if the goal for the "end-user" is invisibility/integration/implicit behaviour (which I think all mean roughly the same thing in this context), then we're in agreement.
Yes, I wasn't disagreeing with you, just pointing out that it might not be easy. As you say, some of this will just disappear because it's low level and will be buried right at the bottom where you implement the basic type operations. Hopefully it'll all turn out like that, but I'm not counting my chickens yet.
Aha! I think I've got it. Here's my explanation of what it's doing (I had to write all this just to work it out!)
First, the value of (catch throw) is stored in k. The catch function does this: get the current continuation and bind it locally to the symbol throw. So we get the current continuation, bind it to throw, and then return throw. So all we've done is return the current continuation at (catch throw).
In the next line, we print 1, so our output looks like this:
=> 1
In the next line, (catch (k throw)), we again get the current continuation and bind it locally to throw. But this time we call the continuation stored in k, passing throw as an argument. This rewinds us back to the beginning, so that the original (catch throw) now returns the value of throw in (catch (k throw)). This value, of course, is the current continuation at the line (catch (k throw)). This new continuation will become the new value of k.
Now you have to remember this about the continuation at (catch (k throw)): although we've rewound the stack back to the beginning and reassigned k, this continuation was created when k had the value it originally had, and it remembers this original value because of lexical scoping.
Anyway, back to the action. We've rewound back to the beginning and stored the continuation at (catch (k throw)) in k. Execution continues at (pr 1), so we get another 1 in our output.
=> 11
Now we get to (catch (k throw)) again. This time, k is the continuation originally created at (catch (k throw)), so we jump back to this line (i.e. the same line), but remember, we've gone 'back in time' (spooky music) to when k had its original value. We continue execution at (pr 2):
=> 112
and then get to the final (catch (k throw). Remember, k now has its original value in this context, so we jump right back to the start again at (catch throw) and give k yet another new value: the continuation at the second (catch (k throw)). Remember, in the context of that continuation, k is still set to its original value! (athough this isn't important because we're not going to use it.)
Now we are at the top, so (pr 1) gets executed again:
=> 1121
Then we get to (catch (k throw)). At the present time, k is bound to the continuation at the second (catch (k throw)), so we now go back in time again to when we were there. The second (catch (k throw)) returns its value (which is lost) and we get on to the next line, (pr 3)
=> 11213
Finally, we have reached the end and return nil, so the final output is:
Thank you very much for explaining this, but there's one thing I don't understand.
The original value of k is the continuation of (catch throw) - a procedure which takes one argument, goes back to the let, and returns its argument as the value of (catch throw). This makes sense.
The second value of k is the continuation of (catch (k throw)). As far as I can tell, this expression does not return a value to anything. Why does its continuation take an argument? Is it just throwing it away?
Yes, the value of (catch (k throw)) is just discarded, so you could pass anything to the continuation and it wouldn't matter. The only reason it takes an argument is because all expressions in Arc have to return something, even if the value isn't used.
The CL boolean type is kind of how I'd like characters to work. Characters would be characters, but they'd also be length one strings, same as how T is a boolean, but it's also a symbol.
I agree that special characters require a different representation (using "\n" and so on all the time is not nice). Perhaps though they could still be strings, but just written with a different syntax?
I know I'll take a lot of flak for this, but I'm going to disagree vehemently with this suggestion. Someone has to play devil's advocate, and I guess it's going to be me :)
One of the things I've liked about Arc so far is the tiny number of types: characters, strings, symbols, numbers, lists, hashes, functions and macros. Having a very small number of types means that every function can be written to do something useful on each type. For example, I just wrote a library for converting any Arc value into its JSON equivalent so I could send them to a web client. It was really easy, because I had so few types to deal with. No need for polymorphic whatnot and abstract do-daas. Just use a simple case statement and you're there.
Elaborate type systems can solve a lot of problems. For example, I had a problem with alists, because I wanted them to be converted into objects, but general lists needed to be converted to arrays. How do you tell the difference between an alist and a normal list? If you had some kind of type system that allowed you to declare that your list is an alist then that would solve it. This is how complex type systems get started, but it's all downhill from there. Once your language starts relying on its type system for its power, you begin taking huge risks.
The problem is that when complex type systems fail, they fail hard. Despite endless academic papers on the subject, and a plethora of practical programming languages, no one has yet developed a type system that is powerful enough to allow people to express everything they want in a reasonably complex program. That wouldn't be a problem if the type system got you 99% of the way there and you had to do a small workaround for the last 1%. Unfortunately, what usually happens is that the 1% left over breaks the whole program. Suddenly you can't use that really useful parsing library, because it's defined over an abstract type that your type can't quite be mapped into. Suddenly the polymorphism features of your language become useless because your type isn't quite polymorphic in the way the language designers anticipated (perhaps you want function polymorphism but the language supports object polymorphism, or vice versa). All type systems fail eventually, and if your language relies on its type system for its power, then you might as well be writing machine code for all the use it will be.
Let's take just two examples. OOP started with multiple inheritance, but then people decided this was too error prone, so people moved so single inheritance. But that wasn't expressive enough, so we got interface inheritance, then generics, then mixins, then "composition over inheritance". Despite all this, you still can't express the relationship between circles and ellipses in any OO language.
Example 2: Haskell was inspired by the very powerful Hindley-Milner type system, which has type variables, polymorphic types and abstract types, but this still wasn't good enough, so they added classes. The ML people still didn't think this was good enough, so they added functors. Some people still didn't think this was good enough, so they proposed higher-level functors. There are endless proposals to add generics to the language too, and then there's dependent typing and all that malarkey. No matter how powerful your type system is, it's never enough.
Arc doesn't get its power from having a complex type system. It gets it from being able to express computations on a small number of types in a very concise manner. This kind of power can fail too, but it doesn't fail hard because Arc has the tools to make any problem simple: functions, macros and reprogramming the programming language. Type systems can save you writing lots of similar functions for slightly different types, but so can macros because they can generate the code for you. Type systems can distinguish between different implementations of cdr and car for different types, but you can also do that by redefining cdr and car to cope with those types, or by passing your own versions of cdr and car along with the object (perhaps even stored in the object itself: see the appendix on object systems in ANSI Common Lisp).
Complex type systems give you power, but they are a seduction that we should resist. They allow you to solve problems by constructing interesting types, but the set of problems you can solve is always strictly limited by the type system (and just because you can write your own type system doesn't mean the problem goes away, unless you can get your system to work seamlessly with everyone else's system).
A powerful set of functions over a small number of types is a much better way of doing this. Types are the sockets that allow functions to be connected together. You can either work with a small number of types that allow you to connect all your functions to each other, or you can create all kinds of different types and then use polymorphism/inheritance/type variables/has-a relationships to try and patch up the fragmentation problem. If you really have lots of types that are similar enough for your functions to work across them, then you should probably use fewer types.
On the other hand, I could be talking nonsense. Let's assume that until we have evidence to the contrary :)
Wow, I think you won the price of the longest post so far. And it is even a very clever one, actually. And I think your view and almkglor's are not so far from each other.
You state that there should only be the few basic types currently defined in Arc. Paul's idea was to eventually get rid of strings (they are a special kind of list) and even numbers (they are a special kind of list too...). But he finally didn't, and won't, at least for numbers. He also said that this view (as few basic types as possible) finally forced him to develop a basic type system (with annotate, type, rep and isa) to distinguish between the raw list '(a a a a a) as the number 5, as the string "aaaaa" or as an actual list of 5 symbols.
In a way or another, you need explicit types if you want some kind of dynamic typing. Assembly language work the opposite way : e.g. you state (explictly or not) the arg of your function is a number. If the user gave you what he considers a string, too bad for him, because you can't distinguish between them. That means your function can't be polymorphic and you are stuck in an even more contrived space than with user types.
You need an isa function (call it isa or hasa, never mind as for now). For example, car should have a list, and nothing else. To do so, you have to check its type. If you want to redefine car so as to take scanners, generators, ... into consideration, that's easy too : just define your own version of car : if arg is a list, call the original car, else arg is a generator à la Python, so funcall it :
(let _ car car
(def car (x)
(if (isa x 'list)
(_car x)
((x)))))
Ok, you're right until now cchooper, predefined types are enough for these situations, and that's how we should do in such cases. Now imagine we want to deal with lists, generators and arrays defined through FFI. You have to distinguish between the latter two, but how can you do that ? Encapsulating them in a cons whose car is a discriminant between both types will not work here, as a cons isa 'list. That's why you need a way to define these new types, and that is what annotate is for.
(let _car car
(def car (x)
(if (isa x 'list)
(_car x)
(isa x 'generator)
((rep x))
(isa x 'array)
(a-get x 1)
(err "Not a valid type for car : " type.x))))
Now, about the distinction between isa and hasa. The wonderfull thing about annotate is that it is very generic ! It does not provide you with a way to say your data is of a specific type, it lets you annotate your data with whatever data you want ! The fact that it works with isa is a side effect actually, annotate does not care about isa. You can annotate with a symbol for sure, but also a string, a number, a list, a macro, a closure, a continuation, whatever !
That means you can do something this way, for example :
And you've got an object system where the car function is embedded into the data when you don't apply it to lists. That's it, you used annotate as it is now defined to create an has-a behavior. Almkglor has got many other funny ideas with typing, and I think all of them can be implemented simply with annotate and encapsulating old definitions of core functions and axioms into usertype-aware ones.
Maybe Arc needs a few more facilities right there (for example, having to use rep on annotated data is, I think, the biggest mistake of that type system. Please, pg, correct this !), but I think we've got everything we need. Almkglor only proposes a few macros and discipline in librarys, but this can be done with the current language definition (and ignored by everybody but him :)
> Wow, I think you won the price of the longest post so far.
Well someone has to load-test this thing!
> And it is even a very clever one, actually.
Thanks :)
> having to use rep on annotated data is, I think, the biggest mistake of that type system.
This is exactly this problem that got me thinking about types. I've been tempted to create a few types with annotate already, but each time I stopped because I didn't want to have to reimplement every function to work on my new type. Each time, I found a different solution to the problem that didn't require new types, which started me thinking "hey, perhaps we don't need new types after all!"
But you're right that you'll always need new types eventually. The solution you suggested is, I think, the right one. It's a bit like the object system in ANSI Common Lisp (pg even used hash tables to store the object's methods!) but it uses annotate to associate methods with objects.
So I'll modify my position and say that you should avoid creating new types, but if you have to do it, duck typing is the way to go.
I particularily like that one : "I expect type names will ordinarily be symbols, but they don't have to be. Either argument can be of any type. I can't imagine why users would want to have type labels other than symbols, but I also can't see any reason to prevent it."
You know, the first thing I thought when I read that x years ago was "You could pass around a load of functions as the type to do polymorphism"! I wonder if everyone has the same thought.
The point mostly is that those problems probably stem from is-a semantics. It might be that has-a semantics might work better.
In such a case a has-a semantics means that circle "has-a" function to compute its area, a function to compute its circumference, etc. An ellipse "has-a" function to compute its area, a function to compute its circumference, etc. It doesn't matter whether the user thinks of circles as special cases of ellipses or not.
Perhaps the way to go would be to support interfaces without requiring type checking. Basically you simply say "all I care about is that this object can be passed to that function".
That said a semiformal way of expressing this - probably by giving a name to an interface (which is just a set of function symbols that an object supports) - might be useful. This way wouldn't necessarily be checked by the program - it might be useful to have it be read by the programmers as part of the code/documentation.
(typeclass 'scanner 'car 'cdr)
; programmer reads: a scanner is anything that somehow supports 'car and 'cdr
(def foo (a)
" A ridiculously complex library function which does
a lot of useful things and which the library user
probably doesn't want to read in full, because he or
she is using the library so that he or she doesn't
have to think about it.
See also [[bar]] "
(must-have a 'scanner)
; programmer reads:
; "Anything I define which supports 'car and
; 'cdr can work on this function"
(ridiculously-complex-expression-involving a))
OK, maybe I flipped out a bit when I heard the word 'type' mentioned. It brought up nightmares of using Java and C++ and so forth.
The kind of thing you're suggesting here does look powerful (and most importantly, optional!) If you combine it with sacado's suggestion for putting the methods in the tag, then you have a very powerful type system indeed.
I just have one contentious thing left to say: when you move away from is-a typing to has-a typing, does it really make sense to use the word 'type' at all? Aren't we really talking about what your functions can do, rather that what your objects are? For example, if you define car and cdr to work on strings, have you added strings to a new sequence type or have you expanded the power of your functions? I prefer to think that you've done the latter, and save the word 'type' for the basic is-a types that every language has to have.
It's the word 'type' I'm objecting to now, not the general idea. Perhaps we need to get out of the typing mindset in order to really break new ground.
The value of types is the name you associate with an object. Instead of giving a really long list of "functions that should work on the object" you say "an object of this type". So instead of saying "an object that has 'car and 'cdr" you just say "scanner".