Arc Forumnew | comments | leaders | submit | fallintothis's commentslogin
3 points by fallintothis 5625 days ago | link | parent | on: Ideas about arc object system.

Arc has something you might call an "object system" -- a bunch of macros down to hash-tables.

  (deftem super1
    field1 'default1)

  (deftem super2
    field2 'default2)

  (deftem (sub super1 super2)
    field3 'default3)

  arc> (inst 'super1 'field1 100)
  #hash((field1 . 100))
  arc> (inst 'super2)
  #hash((field2 . default2))
  arc> (inst 'sub 'field1 100 'field3 300)
  #hash((field1 . 100) (field2 . default2) (field3 . 300))
More thorough documentation: http://files.arcfn.com/doc/template.html

-----

2 points by evanrmurphy 5621 days ago | link

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).

ylando, what do you think about 'deftem?

-----

1 point by ylando 5619 days ago | link

In my own opinion:

Shallow object systems have an advantage of better libraries design; But Arc need a real object system.

Arc need all the basic features for some one to take it seriously.

-----

2 points by rocketnia 5618 days ago | link

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?

-----

3 points by ylando 5617 days ago | link

In my own opinion:

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:

  add-worker, add-statistic, add-html-text ...
Now you have to remember all those names.

-----

3 points by rocketnia 5617 days ago | link

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.

-----

1 point by ylando 5617 days ago | link

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.

-----

1 point by rocketnia 5616 days ago | link

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. ^_^;

-----

0 points by ylando 5616 days ago | link

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.

-----

1 point by evanrmurphy 5619 days ago | link

> 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
test.arc.scm

  ((lambda (x) (begin (let ((zz 10)) (set! x zz) zz))) 5)

-----

2 points by fallintothis 5627 days ago | link | parent | on: Wish list.

I figure it's something like

  (mac scope exprs
    (trav exprs [if (caris _ '@)
                    (list (self (cdr _)))
                    (cons (car _) (self (cdr _)))]))
So then

  arc> (macex1 '(scope let x 2 @ let y 4 @ prn (* x y)))
  (let x 2 (let y 4 (prn (* x y))))
I think the idea is to reduce parentheses, but historically these sort of tricks don't seem to pan out.

-----

1 point by rocketnia 5627 days ago | link

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.)

-----

2 points by fallintothis 5627 days ago | link

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.

-----

2 points by rocketnia 5626 days ago | link

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.

-----

2 points by fallintothis 5626 days ago | link

(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.")))

-----

4 points by fallintothis 5646 days ago | link | parent | on: 'disktem

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:

  (def freadfile1 (name)
    (unserialize (readfile1 name)))
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:

  (def fwrite (val (o output (stdout)))
    (write (serialize val) output))

  (def fwritefile (val name)
    (writefile (serialize val) name))
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).

  (mac load-snapshot (var init)
    `(fromdisk ,var ,(string var) ,init freadfile1 fwritefile))

  (mac save-snapshot (var)
    `(todisk ,var))
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.

  (= persisted-vars* nil)

  (mac persisted (var init)
    `(do1 (load-snapshot ,var ,init)
          (pushnew ',var persisted-vars*)))
Finally, why not make autosave-state thread itself, instead of having to remember to call new-thread?

  (def autosave-state ()
    (thread
      (while t
        (each var persisted-vars*
          (eval `(save-snapshot ,var)))
        (sleep 300))))
Altogether, that's:

  (def freadfile1 (name)
    (unserialize (readfile1 name)))

  (def fwritefile (val name)
    (writefile (serialize val) name))

  (mac load-snapshot (var init)
    `(fromdisk ,var ,(string var) ,init freadfile1 fwritefile))

  (mac save-snapshot (var)
    `(todisk ,var))

  (= persisted-vars* nil)

  (mac persisted (var init)
    `(do (load-snapshot ,var ,init)
         (pushnew ',var persisted-vars*)))

  (def autosave-state ()
    (thread
      (while t
        (each var persisted-vars*
          (eval `(save-snapshot ,var)))
        (sleep 300))))
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:

  (= persisted-vars* nil)

  (mac persisted (var init)
    (w/uniq (val file)
      `(do1 (fromdisk ,var
                      ,(string var)
                      ,init
                      unserialize:readfile1
                      (fn (,val ,file) (writefile (serialize ,val) ,file)))
            (pushnew ',var persisted-vars*))))

  (def autosave-state ()
    (thread
      (while t
        (each var persisted-vars*
          (eval `(todisk ,var)))
        (sleep 300))))
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.

-----

2 points by akkartik 5646 days ago | link

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.

Update: I'm reminded of an earlier bit of code review from you that was super useful as well: http://arclanguage.org/item?id=10700

Also, folks may find it interesting to compare your solution and mine with a version from several months ago: http://arclanguage.org/item?id=10699

Update 2: Ah, fromdisk isn't in the arclanguage.org documentation! That helps me understand where my efforts have been lacking.

-----

1 point by akkartik 5646 days ago | link

"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.

-----

4 points by fallintothis 5657 days ago | link | parent | on: Unsafe-def

I don't know how to make the top-level error handler "disp" instead of "write" the error message, but you see the idea.

I always thought it was silly for Arc to use write for error messages. It seems deliberate, but I'm not sure why. The fix is simple:

  $ diff -u old-ac.scm new-ac.scm
  --- old-ac.scm  2010-05-22 21:32:25.000000000 -0700
  +++ new-ac.scm  2010-05-22 21:34:04.000000000 -0700
  @@ -1137,7 +1137,7 @@
     (on-err (lambda (c)
               (set! last-condition* c)
               (display "Error: ")
  -            (write (exn-message c))
  +            (display (exn-message c))
               (newline)
               (tl2))
       (lambda ()

-----


There's the discussion at http://arclanguage.org/item?id=10157, though I don't know how much has changed since then.

-----

1 point by fallintothis 5666 days ago | link | parent | on: Jaclyn: The Jarc Compiler

I'm sure there's a pg post somewhere around here about it being a bit of effort to get things to work this way, but I can't find it.

http://arclanguage.org/item?id=10248

-----

2 points by fallintothis 5666 days ago | link | parent | on: Jaclyn: The Jarc Compiler

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 () '(1)) ; note: just quote, no quasiquote
  #<procedure: foo>
  arc> (= ((foo) 0) 2)
  2
  arc> (foo)
  (2)
but

  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).

-----

1 point by jazzdev 5665 days ago | link

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 '(...)

  Jarc> (qq-expand '(foo))
  (quote (foo))  ; literal
  Jarc> (qq-expand '(foo bar))
  (quote (foo bar)) ; literal
  Jarc> (qq-expand '(foo nil))
  (list (quote foo) nil) ; not a literal
But Arc does expand `(foo nil) into a literal

  arc> (def foo(s) (let a `(3 nil) (= (car a) (cons s (car a)))))
  #<procedure: foo>
  arc> (foo 2)
  (2 . 3)
  arc> (foo 1)
  (1 2 . 3)

-----

2 points by aw 5664 days ago | link

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.

-----

1 point by fallintothis 5665 days ago | link

That's a bug. It didn't think that nil was a constant. Apparently I didn't know about literal 327 days ago. Thanks for finding it. :)

http://bitbucket.org/fallintothis/qq/changeset/6c0dab5f091e

(Really, all of that code needs a good rewrite. It was straight-ported from some Common Lisp, and suffers greatly for it. Bleh.)

-----

3 points by fallintothis 5664 days ago | link

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.

http://bitbucket.org/fallintothis/qq/src/tip/qq.arc

-----

2 points by aw 5664 days ago | link

Yay!

-----

2 points by fallintothis 5666 days ago | link | parent | on: Negative indexes for strings

  arc> (ssexpand 'xs.-1)
  (xs -1)
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

-----

1 point by evanrmurphy 5666 days ago | link

Glad the 'ssexpand works, I should have tried that.

Python's indexing doesn't wrap around infinitely, but that doesn't mean Arc's couldn't.

-----

1 point by fallintothis 5666 days ago | link

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.

-----

1 point by evanrmurphy 5666 days ago | link

> 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.

-----

2 points by fallintothis 5672 days ago | link | parent | on: What does 'nil buy us?

http://hacks.catdancer.ws/nil-impl-by-null.html

-----

1 point by akkartik 5672 days ago | link

I basically went through the same process 2 days ago! And yes it works.

-----

More