With closures and 'deftem together, I haven't yet had the need for much more of an object system in Arc. These templates are great because they're so transparent, though I guess I've sometimes wished they were more robust (e.g. http://arclanguage.org/item?id=11855).
What do you like about having an object system, specifically? I wouldn't mind having one too, but I think object systems get complicated and arbitrary very quickly. I'd rather not have one monolithic abstraction where several highly-focused ones would do.
I do find myself switching to Groovy a lot thanks to its object system. I think what I mainly like is the ability to make an instance of FooSubclass and automatically have "foo in FooSuperclass" and "foo in FooInterface" be true.
Using Arc's built-in notion that the type of an object is the result of calling the 'type function on it (which is configurable via 'annotate), it's easy enough to achieve a form of inheritance that accomplishes what I just described:
; This is a table from type symbols to lists of their supertypes.
; Indirect supertypes are included in these lists, but no type is a
; member of its own list. There are no inheritance loops, either;
; inheritance here is a directed acyclic graph, and this is the
; transitive closure of that graph.
(= indirect-inheritance* (table))
(def inherits (subtype supertype)
(~~mem supertype (cons subtype indirect-inheritance*.subtype)))
(def fn-def-inherits (subtype . supertypes)
(each supertype supertypes
(when (~inherits subtype supertype)
(when (inherits supertype subtype)
(err "There was an inheritance loop."))
(let supers (cons supertype indirect-inheritance*.supertype)
(zap [union is supers _] indirect-inheritance*.subtype)
(each (k v) indirect-inheritance*
(when (mem subtype v)
(zap [union is supers _] indirect-inheritance*.k)))))))
(mac def-inherits (subtype . supertypes)
`(fn-def-inherits ',subtype ,@(map [do `',_] supertypes)))
(def isinstance (x test-type)
(inherits type.x test-type))
(def a- (test-type)
[isinstance _ test-type])
Come to think of it, I can't think of anything else object-oriented I miss in Arc. (OO is good for dispatch too, but I've already made an Arc dispatch library I like better.) I'll make sure to try this out the next time I program something substantial in Arc.
So, I've talked a lot about me, but my question to you still stands. What features are you looking for in an object system?
The most important feature of object oriented is name compressing.
Imagine a program with a lot of different databases.
Now you have to give names for functions that add element,
You will have to give them names like:
That sounds like dispatch to me. ^_^ You'd like to use one name but give it multiple meanings based on the types involved. This thread (http://arclanguage.org/item?id=11779) has a couple of approaches to dispatch from akkartik and myself.
Hmm, I've been having trouble for a while, trying to figure out my favorite way to have inheritance and multiple dispatch at the same time. (Single dispatch with inheritance seems more straightforward, but I'd rather not bother with it if I could use multiple dispatch instead.) If one method signature is (A, B) -> C, and another is (B, A) -> C, and A is a subtype of B, then what happens if you call the method with (a, a)? That could just cause an error, or the first argument could take precedence, but I'm not sure which approach I like better.
I think that multi dispatch is good idea for strong type languages.
If c++ encounters this problem it will solve it at
compile time (I think it will try to find the solution that
is best for the first variable then the second and so on).
For a dynamic language like arc multi dispatch
will have an efficiency cost. In my own opinion:
It is one of the "too smart to be useful" features of
common lisp. It is one of the reason people hate clos.
I have a question for you:
In what way a multi dispath is better then the alternatives?
You can dispatch only on the first argument
or hold a virtual table of functions for every object.
For a dynamic language like arc multi dispatch will have an efficiency cost.
I don't think the efficiency cost for dynamic dispatch on the first argument would be significantly different, and I'm not worried about either one. I think they're at worst as inefficient as a sequence of if statements, which is to say they would take constant time (given a constant number of methods to check and constant time for each check).
Constant time can still be painful in the right quantity, and making the decision at compile time would indeed help, but Arc doesn't have the necessary static type information to go on (as you know). So multiple dispatch and single dispatch alike have to suffer a bit.
It is one of the reasons people hate clos.
Well, I've heard nothing but good things about CLOS, but then I haven't heard that much. ^_^ Is there some good anti-CLOS reading material you can recommend? XD
In what way is multi dispatch better then the alternatives?
It's partly a matter of name compression. ^_-
; no dispatch
(collide-spaceship-with-spaceship s s)
(collide-spaceship-with-asteroid s a)
(collide-asteroid-with-spaceship a s)
(collide-asteroid-with-asteroid a a)
; single dispatch (on the first argument)
(collide-with-spaceship s s)
(collide-with-asteroid s a)
(collide-with-spaceship a s)
(collide-with-asteroid a a)
; double dispatch
(collide a s)
(collide s a)
(collide a s)
(collide a a)
Yeah, I stole Wikipedia's example here. Another example is a comparison function that needs to compare values of many different types. Yet another is a "write" function which has different behavior for each type of writer object and each type of object to be written.
When it comes right down to it, I just believe multiple dispatch ought to be a simpler concept than single dispatch, since it imposes fewer restrictions on the programmer. Unfortunately, it seems to replace one arbitrary restriction with a bunch of (what I see as) arbitrary design decisions. ^_^;
For every one of your examples,
we can use double dispatch design pattern;
For example:
(collide o1 o2) call
(collide-with-spaceship o2 o1) if o1 is a spaceship
or (collide-with-asteroid o2 o1) if o1 is an asteroid.
I think that you can even abstract this design pattern
with a macro (but I did not try it).
So you still did not convince me that multi dispatch is
a good feature.
> Arc need all the basic features for some one to take it seriously.
As long as implementing any feature for the now doesn't compromise Arc's quality in the long term, I doubt anyone here would disagree with you. Keep in mind though that Arc's core language is still subject to change (http://www.paulgraham.com/core.html).
ac will have compiled Arc lexical variables down to Scheme lexical variables. So, they'll already be bound -- no need for define. E.g.,
test.arc
(let x 5
(assign x 10))
At the REPL
$ mzscheme -if as.scm
Welcome to MzScheme v4.2.1 [3m], Copyright (c) 2004-2009 PLT Scheme Inc.
Use (quit) to quit, (tl) to return here after an interrupt.
arc> :a
> ((lambda (x) (set! x 10) x) 5)
10
> (acompile "test.arc")
#t
I've actually wanted something similar from time to time, but without the '@ and 'let. Right now, I have just one place I really miss it (which you can see at http://github.com/rocketnia/lathe/blob/c343b0/arc/utils.arc), and that place currently looks like this:
(=fn my.deglobalize-var (var)
(zap expand var)
(if anormalsym.var
var
; else recognize anything of the form (global 'the-var)
(withs (require [unless _
(err:+ "An unrecognized kind of name was "
"passed to 'deglobalize-var.")]
nil (do.require (caris var 'global))
cdr-var cdr.var
nil (do.require single.cdr-var)
cadr-var car.cdr-var
nil (do.require (caris cadr-var 'quote))
cdadr-var cdr.cadr-var
nil (do.require single.cdadr-var)
cadadr-var car.cdadr-var
nil (do.require anormalsym.cadadr-var))
cadadr-var)
))
(Note that the Lathe library defines 'anormalsym to mean [and _ (isa _ 'sym) (~ssyntax _)]. Also, 'my is a Lathe namespace here, and I use Lathe's straightforward '=fn and '=mc to define things in namespaces. Finally, don't forget that (withs (nil 2) ...) binds no variables at all ('cause it destructures).)
If I define something like 'scope, I save myself a few parentheses and nils:
(=mc my.scope body
(withs (rev-bindings nil
final nil
acc [push _ rev-bindings])
(while body
(let var pop.body
(if no.body
(= final var)
anormalsym.var
(do do.acc.var (do.acc pop.body))
(do do.acc.nil do.acc.var))))
`(withs ,rev.rev-bindings ,final)))
(=fn my.deglobalize-var (var)
(zap expand var)
(if anormalsym.var
var
; else recognize anything of the form (global 'the-var)
(my:scope
require [unless _
(err:+ "An unrecognized kind of name was passed "
"to 'deglobalize-var.")]
(do.require (caris var 'global))
cdr-var cdr.var
(do.require single.cdr-var)
cadr-var car.cdr-var
(do.require (caris cadr-var 'quote))
cdadr-var cdr.cadr-var
(do.require single.cdadr-var)
cadadr-var car.cdadr-var
(do.require anormalsym.cadadr-var)
cadadr-var)
))
Furthermore, I suspect this version of 'scope would almost always be preferable to 'do, 'let, and 'withs, partly because it's easier to refactor between those various cases. However, it doesn't do any destructuring, and it's probably a lot harder to pretty-print. (I'm not even sure how I would want to indent it in the long term.)
That example doesn't really call out to me: it looks like you could save yourself a few parentheses and nils by refactoring with something simpler, rather than with a complex macro. E.g., if I understand the code correctly:
(=fn my.aglobal (var)
(and (caris var 'global)
(single:cdr var)
(caris var.1 'quote)
(single:cdr var.1)
(anormalsym var.1.1)))
(=fn my.deglobalize-var (var)
(zap expand var)
(if (anormalsym var)
var
(aglobal var)
var.1.1
(err "An unrecognized kind of name was passed to 'deglobalize-var.")))
But then, I avoid setting variables in sequence like that.
Hmm, I kinda prefer local variables over common subexpressions. It's apparently not for refactoring's sake, since I just name the variables after the way they're calculated, so it must just be a premature optimization thing. :-p
But yeah, that particular example has a few ways it can be improved. Here's what I'm thinking:
(=fn my.deglobalize-var (var)
(zap expand var)
(or (when anormalsym.var var)
(errsafe:let (global (quot inner-var . qs) . gs) var
(and (is global 'global)
(is quot 'quote)
no.qs
no.gs
anormalsym.inner-var
inner-var))
(err:+ "An unrecognized kind of name was passed to "
"'deglobalize-var.")))
I still like 'scope, but I'm fresh out of significant uses for it.
(To continue the digression into this particular case...) I had thought about destructuring, but found the need for qs and gs ugly. Really, a pattern matching library would be opportune. But since there's no such luck in Arc proper, it'd be ad-hoc and probably altogether not worth it (though not difficult to implement). I say this with respect to vanilla Arc; I don't know if Anarki has such a library. Still, it'd be hard to beat something like
(=fn my.deglobalize-var (var)
(zap expand var)
(or (check var anormalsym)
(when-match (global (quote ?inner-var)) var
(check ?inner-var anormalsym))
(err "An unrecognized kind of name was passed to 'deglobalize-var.")))
Sorry for the unsolicited code review (I don't mean to be malicious), but the implementation seems overcomplicated: wheel reinvention that's inconsistent with Arc's built-ins.
Why is fread a macro? You can easily set a variable explicitly when you want it. Moreover, the basic functionality is like Arc's readfile1, not read. It can simplify to a composition of two functions:
If you wanted something more like readfile that unserializes each form, you can just amend Arc's definition:
(def freadfile (name)
(w/infile s name (drain (unserialize (read s)))))
I suppose there's (map unserialize (readfile name)), but that's an extra pass with more consing.
fwrite and fwritefile appear to duplicate functionality -- one just makes a temp file first. I assume you got the idea from Arc's writefile, except you added (until file-exists.tmpfile). Is there a reason for this? Did you have issues with trying to move the file before it was finished writing?
That point notwithstanding, they can still be rewritten closer to Arc's built-ins:
The f prefixing all these names confuses me. What is it supposed to stand for? "File"?
load-snapshot uses stringify, which I assume is supposed to be Arc's string (which works just fine for converting symbols to strings). It also has a variable capture issue since it uses aif. E.g., using your original definition with (= stringify string):
arc> (load "macdebug.arc") ; see http://www.arclanguage.org/item?id=11806
arc> (macsteps '(load-snapshot it init) 'show 'load-snapshot 'aif)
Showing only: load-snapshot, aif
Expression:
(load-snapshot it init)
Macro Expansion:
(load-snapshot it init)
==> (aif (file-exists "it")
(fread it it)
(= it init))
Expression:
(aif (file-exists "it")
(fread it it)
(= it init))
Macro Expansion:
(aif (file-exists "it")
(fread it it)
(= it init))
==> (let it (file-exists "it")
(if it (fread it it) (= it init)))
Expression:
(let it (file-exists "it")
(if it (fread it it) (= it init)))
nil
Really, what's happening between load-snapshot and save-snapshot is encapsulated by Arc's fromdisk and todisk (added benefit: no variable capture).
persisted can also use do1 to be more like fromdisk. Instead of returning the list of persisted variables, it will return the actual variable's value. I don't know if your behavior was intentional, but it's an easy change.
This is more consistent with Arc's counterparts and makes good use of existing functionality. Indeed, load-snapshot is Arc's diskvar, except we lace it with serialize & unserialize to handle different structures generically, which is useful.
It could be shorter if we accept freadfile1, fwritefile, load-snapshot, and save-snapshot as implementation details of persisted:
Anyway, I think the autosaving and un/serialize generic combo is pretty cool. It lets you extend the functionality more than Arc's existing fromdisk stuff, since you just make a new generic function to handle your data; then you get to keep using persisted.
Thanks a lot. My implementation is an accretion since I first started playing with arc. I no longer remember what the 'f' prefix stands for :) Perhaps it should be gread and gwrite for generic or generalized..
I'm going to spend a while going over the methods you pointed out - I still wasn't familiar with readfile1 or fromdisk. Thanks. Code review is always appreciated.
"you added (until file-exists.tmpfile). Is there a reason for this? Did you have issues with trying to move the file before it was finished writing?"
Just looked through my changelogs. The answer is yes, I seem to have run into this issue at some point. writing is a disk-level creature where mv is a file-system-level beast. That seems to cause occasional issues.
Yeah, I'm disinclined to try to emulate Arc precisely in this regard because it appears to be an arbitrary artifact of how Arc does quasiquote expansion, and not a defined feature.
I think the only thing going on here is quotation. Generally, quoted things work like pointers (see http://arclanguage.org/item?id=10248). Because you can still do
arc> (def foo () (cons 1 nil)) ; note: cons creates new data each call
*** redefining foo
#<procedure: foo>
arc> (= ((foo) 0) 2)
2
arc> (foo)
(1)
Then, the reason the quasiquoted thing gets weird results with the qq.arc I ported is that optimizing `(1) manages to switch between always consing new data with (list '1) and trying to reduce runtime consing with '(1).
I'm not (terribly) surprised that the ability to modify literals was intentional. Thanks for that link. I'm still wondering if `(1) expanding to '(1) is intentional or not.
Clearly '(foo) is a literal and `(foo ,bar) is not a literal, right?
I guess it's not a huge stretch to say that `(foo) is a literal. Unfortunately, qq-expand doesn't always expand `(...) with no commas into '(...)
I'm still wondering if `(1) expanding to '(1) is intentional or not.
A quasiquote expander can expand `(1) into '(1) or (list 1), or even (join '(1) ' nil), which is what Bawden's simple, correct, but inefficient quasiquote expander does.
The expander may choose '(1) as being more efficient than (list 1), but isn't required to do so to be a quasiquote expander.
Since it's an optimization, I wouldn't write code that relies on a particular instance of `(1) evaluating to the same list every time, that's just an accidental result of the optimization. Instead, use a plain quote for that.
There. Just updated qq.arc, removing almost 100 lines of cruft in the process. It still passes all the old tests, and (with any luck) is much more pleasant to work with.
To model indexing after Python, valid indices for a sequence of length n would be integers in the range [-n, n).
>>> xs = ['a', 'b', 'c']
>>> xs[0] is xs[3]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IndexError: list index out of range
>>> xs[0] is xs[-6]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IndexError: list index out of range
>>> xs[0] is xs[-3]
True
Hence "To model indexing after Python". I figure Python's probably a good source of inspiration for Python-like features. ;)
Admittedly, I can't find specific rationale for IndexErrors discussed anywhere. Perhaps throwing errors for egregious indices just seems the "sane" thing to do. Essentially, it's a choice of foist: if indexing wraps around infinitely, you need to check bounds when you don't want to wrap around; if indexing throws errors, you need to mod explicitly when you do want to wrap around.
> if indexing wraps around infinitely, you need to check bounds when you don't want to wrap around; if indexing throws errors, you need to mod explicitly when you do want to wrap around.
Good summary. I agree that index-out-of-bound errors are the saner default.