Arc Forumnew | comments | leaders | submitlogin
6 points by almkglor 6108 days ago | link | parent

A wiki, in arc. Name it Arkani and save it in wiki-arc.arc for further confusion ^^ or not, I'm planning to build this myself

A pg-approved way of creating objects whose assignment methods can be changed, preferably one which requires a relatively simple attachment of functions to functions (cref. "Create your own collection in Arc*" and "a (possibly not very important) bug in annotate"). We could make it ad-hoc and force everyone to use (redef sref...) and stuff, or we could make it a pg-defined convention.



5 points by nex3 6108 days ago | link

Yes. defset for annotated objects would be excellent, although I'm skeptical that the optimal way to do this is attaching functions to objects. I prefer a solution closer to how defset works now: some sort of table associating types to setters.

-----

5 points by almkglor 6108 days ago | link

Hmm. In any case I think our disagreement here has more to do with how we view types. It appears that you have the view of "type" as similar to non-abstract classes in C++: Each class defines a set of static functions for accessing class objects. Applying a function to the object dispatches according to the type of the object. An object of another class cannot be used as an object for a different class.

On the other hand, I view "type" as similar to abstract base classes in C++ (or type classes in Haskell). That is, a "type" defines what the object's interface is. The type defines a set of virtual functions which actual implementations of the type must have. So a 'table type must provide a 'keys virtual function, and a '= virtual setterfunction. Applying a function to the object dispatches according to the actual object, instead of the object's proclaimed type.

-----

4 points by nex3 6108 days ago | link

That sounds about right. My thought process was more along the lines of CLOS-style generic functions, but I think these are roughly equivalent to a more generalized notion of C++'s instance methods.

As vehemently as I may argue, I'm not actually entirely convinced that this way is best. I come from a Ruby tradition, which is deeply based on the message-passing object model, which ends up behaving a lot like type classes. I've seen this end up working very nicely in practice, facilitating duck typing and allowing all sorts of cool tricks via encapsulation.

At the same time, though, CLOS is supposed to be very excellent. And the one thing, more than any other, that appeals to me about a generic-function style object system is that it can be implemented in pure Arc. It doesn't require any more axioms than the very-simple type, rep, and annotate (née tag).

The message-passing/type-class model, on the other hand, requires what seems to me to be an incredibly radical change to the core of the language: built-in per-object tables. This just strikes me as fundamentally un-Lispy. In a sense, it eclipses lists as the fundamental data type - they're not much more than tables with "car" and "cdr" keys.

While this may be interesting territory to explore, it's being explored in other languages - Lua and Javascript both explicitly regard hash tables in the same that Lisp regards lists.

Also, from a more practical sense, I'm not convinced that the message-passing/type-class model offers anything that generic functions don't. The main benefit that I've seen is duck typing - the ability of a function to rely on its input implementing to a given interface (in this case, having functions work properly on it), rather than being a given type.

But I think this works just as well whether or not the core functions are implemented with a type-class-style system or a generic-function-style system.

Consider, for example, Ruby's favorite duck type: enumerable objects. In Ruby, every object that implements an each method that calls a lambda on each element can be declared "Enumerable," and get various methods like map for free. This can be done using attachments like so:

  (let (foo bar baz) ("foo" "bar" "baz")
    (add-attachments
      'each (fn (f)
              (f foo)
              (f bar)
              (f baz))
      nil))

  (def each (obj f)
    ((get-attachment 'each c) f))
or using generic functions like so:

  (= foo (annotate 'mytype '((foo "foo")
                      (bar "bar")
                      (baz "baz"))))

  (redef each (obj f)
    (if (~isa obj 'mytype) (old obj f)
        (do
          (= obj (rep obj))
          (f (car obj))
          (f (cadr obj))
          (f (cadr:cdr obj)))))
However, in either case, map can be defined as simply

  (def map (obj f)
    (let l nil
      (each obj [push (f _) l])))
The point of all this being that duck typing (or whatever you want to call the versatility granted by assuming a type implements an interface rather than specifically checking its type) is independent of whether or not the interface is implemented by attaching functions to objects or by defining generic methods.

-----

1 point by sacado 6108 days ago | link

I'd tend to agree with your vision, almkglor. It is more generic than the other way around : the former can be modelized with the latter, but the opposite is not true. I think Arc should remain generic and thus leave us the choice.

-----

2 points by nex3 6108 days ago | link

The problem is that Arc is not currently generic enough to allow both models. Only generic functions can be implemented with the primitives we're given - the core language has to be modified to allow arbitrary attachments.

-----

2 points by almkglor 6108 days ago | link

Attaching a setterfunction to an object allows us to create encapsulating modules which can have specific module variables modified:

  (with (var1 42
         fn1 nil)
    (def fn1 (x)
      (prn var1 ": " x))
    (add-attachments
      '= (fn (v s)
           (case s
             var1 (= var1 v)
                  (err:string "Cannot set module variable: " s)))
      'keys (fn () (list 'var1 'fn1)
      (fn (s)
        (case s
          fn1 fn1
          var1 var1)))))
Note that the above does not even care about generating its own type, because it's really a one-of table.

Also, making use of lexical environment closures severely reduces the amount of code necessary for accessor functions:

  ;Using attachments -
  (def bidir-table ()
    " Creates a bidirectional table.  Works like a normal
      table but returns keys when queried with values.
      See also [[table]] "
    (with (k-to-v (table)
           v-to-k (table))
      (add-attachments
        '= (afn (v k)
             ; determine if delete or assign
             (aif
               ; insert new pair
               v
                 (do
                   ; delete any existing pairs first
                   (self nil k)
                   (self nil v)
                   ; add it
                   ;  no point assigning this to v-to-k
                   ;  if v==k, since k-to-v will return
                   ;  that mapping first
                   (if (isnt k v) (= (v-to-k v) k))
                   (= (k-to-v k) v))
               ; deleted k
               (k-to-v k)
                 (= (v-to-k it) nil
                    (k-to-v k) nil)
               ; deleted v
               (v-to-k k)
                 (= (k-to-v it) nil
                    (v-to-k k) nil)))
        ; Only return items which were assigned as
        ; keys, so that 'ontable doesn't go over
        ; pairings twice.
        'keys (fn () (keys k-to-v))
        (annotate 'table
          (fn (k) (or (k-to-v k) (v-to-k k)))))))

  ;Using defset-type and defcall:
  (define bidir-table ()
    " Creates a bidirectional table.  Works like a normal
      table but returns keys when queried with values.
      See also [[table]] "
    (annotate 'bidir-table (list (table) (table))))

  (defcall bidir-table (c k)
    (let (k-to-v v-to-k) (rep c)
      (or (k-to-v k) (v-to-k k))))

  (defset-type bidir-table (c v k)
    (let (k-to-v v-to-k) (rep c)
      ((afn ()
             ; determine if delete or assign
             (aif
               ; insert new pair
               v
                 (do
                   ; delete any existing pairs first
                   (self nil k)
                   (self nil v)
                   ; add it
                   ;  no point assigning this to v-to-k
                   ;  if v==k, since k-to-v will return
                   ;  that mapping first
                   (if (isnt k v) (= (v-to-k v) k))
                   (= (k-to-v k) v))
               ; deleted k
               (k-to-v k)
                 (= (v-to-k it) nil
                    (k-to-v k) nil)
               ; deleted v
               (v-to-k k)
                 (= (k-to-v it) nil
                    (v-to-k k) nil)))))

-----