Arc Forumnew | comments | leaders | submitlogin
4 points by conanite 5958 days ago | link | parent

You can use lexical scoping to store private state for an object (not accessible via (= thing!foo 'bar) ), and so that your object's functions can call each other without requiring any kind of qualifying prefix. Here's a contrived example of a "shouter" that can be started, stopped, and shouts stuff when started.

  (def shouter (name)
    (with ((this started) nil)
      (= this (let (start stop shout prname equals) nil
                   (= start  (fn ()  (assert started)))
                   (= stop   (fn ()  (wipe started)))
                   (= shout  (fn (s) (if started (pr (upcase s)))))
                   (= prname (fn ()  (shout name)))
                   (= equals (fn (x) (is this x)))
                   (obj start start stop stop shout shout prname prname equals equals)))
       this))

  arc> (set sh1 (shouter "me"))
  arc> (sh1!start)
  arc> (sh1!prname)
  "ME"
"this" is defined so that 'equals will work (otherwise the functions have no way to reference the object that contains them); 'prname can call 'shout directly. 'name and 'started are hidden - 'name cannot be modified, and 'started can only change via the "public" 'start and 'stop functions.

The right macro can disappear all the boilerplate:

  (def shouter (name)
    (with ((this started) nil)
      (= this (make-obj
         (start  ()  (assert started))
         (stop   ()  (wipe started))
         (shout  (s) (if started (prn (upcase s))))
         (prname ()  (shout name))
         (equals (x) (is this x)) ))
    this))
'make-obj was inspired by obj and http://arclanguage.org/item?id=7387 ("if it's an idiom, it needs a macro") ...

  (with ((make-def double-each) nil)
    (= make-def (fn ((name args . body))
       `(def ,name ,args ,@body)))
    (= double-each (fn (lst)
       (if lst
           `(,(car lst) 
             ,(car lst) 
             ,@(double-each (cdr lst))))))
    (mac make-obj args
      `(with (,(map car args) nil)
             ,@(map make-def args)
             (obj ,@(double-each (map car args))))))
Coming from javaland, I fear I'm trying to hack objects onto arc and failing to see the One True Way with functions. But this macro has been convenient for interfacing with java. I guess some seriously tricky macroing would allow inheritance ... this is left as an exercise for the reader :)


4 points by absz 5958 days ago | link

The real problem with storing the functions in the object, I think, is that you have different copies of the function for every object. There are two problems with this: (a) it wastes space, and (b) if you redefine the function, you have to recreate all your objects. That's the advantage of storing methods externally.

Also, speaking of "if it's an idiom, [it] probably needs a macro," I often find myself writing (obj name1 name1 name2 name2 name3 name3 ...), like you have above. So here's my nobj macro to solve that problem; for the same effect as above, just write (nobj name1 name2 name3 ...):

  (mac nobj args
    " Creates a table from the list of variables passed to it; each variable's
      name is keyed to its value.  E.g. if x = 10 and y = -10, then (nobj x y)
      results in #hash((y . -10) (x . 10)).
      See also [[obj]] [[table]] "
    `(obj ,@(flat:map [list _ _] args)))

-----

3 points by tokipin 5958 days ago | link

well, we can closure the methods with the constructor function so they aren't duplicated, at the expense of explicitly requiring the object to be passed to them, or some kind of dispatch mechanism. we can also implement simple inheritance:

  (def inherits (obj1 obj2)
       (fn (key)
           (or (obj1 key) (obj2 key))))
with (apply or ...) we could have a long inheritance chain, but here i have just two objects so the pattern is clear. we can then define objects like so:

  (let _proto (obj
                full (fn (self) (string self!first " " self!last))
                until (fn (self year) (- year self!age)))
  
    (def person (first last age)
           (inherits (obj
                       _proto _proto
                       first first
                       last last
                       age age)
  
                     _proto))
  
  )

  arc> (= p (person "joe" "momma" 18))
  #<procedure>
  arc> (p!full p)
  "joe momma"
  arc> (p!until p 22)
  4
  
  arc> (= q (person "axe" "murderer" 10))
  #<procedure>
  arc> (q!full q)
  "axe murderer"
  arc> (q!until q 22)
  12
because the inheritance is dynamically dispatched or whatitbe, we can alter the methods with the intended effects:

  arc> (q!backwards q)
  Error: "Function call on inappropriate object nil (#<procedure>)"
  arc> (= ((q '_proto) 'backwards) (fn (self) (string self!last " " self!first)))
  #<procedure:gs2439>
  arc> (q!backwards q)
  "murderer axe"
  arc> (p!backwards p)
  "momma joe"

  arc> (= ((q '_proto) 'until) (fn (self age) "a long time"))
  #<procedure:gs2451>
  arc> (q!until q 22)
  "a long time"
  arc> (p!until p 18)
  "a long time"
note the 'inherits' function is agnostic. we can inherit from arbitrary objects and functions or what have you, just that in this case it was used to inherit from a hidden prototype

anarki supposedly has user definable syntatic sugaries, so the (q!blah q) pattern could be sugarized

-----

2 points by almkglor 5958 days ago | link

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

-----

4 points by conanite 5956 days ago | link

Does it really store a new copy of the function each time? I thought it would only store the closure, and apparently closures are cheap ( http://arclanguage.org/item?id=7342 ) (sorry almkglor, I end up quoting you all the time).

And as for redefining functions: once your api is stable there's probably less need to redefine functions, and if necessary you can still

(= p!until (fn () ...))

(although this way you don't have access to "private" variables in the lexical scope of the original. I'm completely with EliAndrewC in preferring (p!until ...) over (person-until p ...)

And thanks for nobj, it's awesome. It makes my macro look like Visual Basic. Slowly, I learn ...

-----

3 points by almkglor 5956 days ago | link

Yes, a good implementation should store just the closed variables and a reference to the code - there shouldn't be any code duplication.

It thus depends on how many local variables are being closed over. Note that in some implementations (although not in arc2c, and by inference not in SNAP) a closure is just two pointers: a reference to an environment and a reference to the code. Of course each 'let form and function would create a new environment though, and this style is not so often used because lookup of closed variables can require indirection.

-----

2 points by absz 5956 days ago | link

You're welcome---I'm glad to have been of assistance.

As for storing copies, I would have said that it would store extra copies because of the different variable it closes over, but almkglor points out that you can separate code and environment, so the question is what mzscheme does.

The thing about redefinition is that in, say, Ruby, you can do

  class String
    def foo
      code_goes_here
    end
  end
And every string will have that new foo method. Here, you can only redefine the methods of one object.

For me, the real syntax question is whether we want (until p age) (which probably means that we are using the CLOS model of generic functions) or we want (p!until age) (which probably means that we are using the Smalltalk model of message passing). I sort of like the former syntax, but I also sort of prefer the Smalltalk model. What do you think?

-----

1 point by almkglor 5956 days ago | link

Note that redefinition using (p!until age) syntax is still possible in Arc using 'defcall and if you predeclare the private variables.

For instance, consider this:

  (deftype foo (x)
    (private y z)
    (meth niaw ()
      (do-something x y z))
    (meth arf (something)
      (do-something-else something x y z)))
  =>
  (let methods
       (table
         ; lambda lifted!
         'niaw
         (fn (x y z)
             (do-something x y z))
         'arf
         (fn (x y z something)
             (do-something-else something x y z)))
    (def foo-replace-method (s f)
      (= (methods s) f))
    ; so external code can determine the local variables
    (def foo-get-private-variables ()
      '(x y z))
    (def foo (x)
      (with (y nil z nil)
        (let invoker
             (fn (f rest)
               (apply f x y z rest))
        (fn (which-method)
          (aif
            (methods which-method)
               (fn rest (invoker it rest)))))))
Then a method redefining macro can be:

  (def lastcons (l)
    (if (cdr l)
        (lastcons:cdr l)
        l))
  (mac redef-meth (type meth params . body)
    (givens replacer (sym:string type "-replace-method")
            privates (eval:list:sym:string type "-get-private-variables")
            _ (= (cdr:lastcons privates) params)
      `(,replacer ,meth (fn ,privates ,@body))))
Note that foo-replace-method and foo-get-private-variables could be placed in a central global table or two instead.

-----

5 points by almkglor 5958 days ago | link

  (mac nobj args
    " Creates a table from the list of variables passed to it; each variable's
      name is keyed to its value.  E.g. if x = 10 and y = -10, then (nobj x y)
      results in #hash((y . -10) (x . 10)).
      See also [[obj]] [[table]] "
    `(obj ,@(mappend [list _ _] args)))
^^

-----

1 point by absz 5958 days ago | link

Even better, then :) And I definitely need to remember that this exists.

-----

3 points by almkglor 5958 days ago | link

^^ Of course you don't: just tell Arc about what you do remember, and it'll tell you more about related things you might want to look at too:

  arc> (help map)
  (from "arc.arc")
  [fn]  (map f . seqs)
   Applies the elements of the sequences to the given function.
      Returns a sequence containing the results of the function.
      See also [[each]] [[mapeach]] [[map1]] [[mappend]] [[andmap]]
      [[ormap]] [[reduce]]

-----

6 points by absz 5958 days ago | link

I know, but when was the last time you thought you needed help with map of all things? :)

Actually, aha! I added this to the bottom of ~/.arcshrc

  (let func (random-elt:keys help*)
    (prn "Documentation for " func " " (helpstr func)))
Now whenever I start arc, it will print, e.g.,

  Documentation for saferead (from "arc.arc")
  [fn]  (saferead arg)
   Reads an expression, blocking any errors. 
  
  Use (quit) to quit, (tl) to return here after an interrupt.
  arc> 
And thus hopefully I will learn something :)

-----

3 points by almkglor 5958 days ago | link

> 'make-obj was inspired by obj and http://arclanguage.org/item?id=7387 ("if it's an idiom, it needs a macro") ...

Lisp boy: Do not try to abstract the idiom. That's impossible. Instead... only try to realize the truth.

Newb: What truth?

Lisp boy: There is no idiom.

Newb: There is no idiom??

Lisp boy: Then you'll see, that it is not the idiom that does abstraction, but yourself.

^^

-----