Arc Forumnew | comments | leaders | submitlogin
Ask: PG and Anti-OO
1 point by d0m 5188 days ago | 7 comments
I've read http://www.paulgraham.com/noop.html (Why Arc isn't Especially Object-Oriented) and I mostly agree on all points.

There is still one argument that I truely appreciate with OO thought (and that wasn't really debated in this article): clear abstractions.

When I jump into a new project, I really appreciate when functionalities are clearly separated -- might it be OO or not. Who likes spending 15mins reading code only to find out that the useful part was somewhere else? (Ok, it depends of the actual code.)

I understand that OO is overkill for many tasks and that arc usually offers better alternatives. However, as pointed out in the article, it doesn't mean that OO is never useful.

For instance, with python, I like the fact that even if String has a "join" method, it doesn't stop me from using that name in my code.

Anyhow, here's my idea about a simple arc-ish OO.

This:

  (let s (Socket "irc.freenode.net" 6667)
    (on-receive s [prn _])
    (send s "nick d0m\n"))
Would be transformed to:

  (let s (Socket "irc.freenode.net" 6667)
    (s 'on-receive [prn _])
    (s 'send "nick d0m\n"))
Socket might be defined this way:

  (defobj Socket arg
    (let o
        (obj 'on-receive (fn (..) ..)
             'send (fn (..) ..))
       (apply (o (car arg)) (cdr arg))))
and defobj could someway let the interpreter know that Socket is an object and that: (send (Socket) ..) should be evaluated as ((Socket) 'send ..)

Arc has hundreds of function and it might feel overwhilming for beginners. This might be a way to guide them through everything Arc has to offer.

Finally, the fact that string are callable such as: ("hello" 0) -> h proves that OO isn't as useless as we might think.. In this example, "hello" is clearly an object.



1 point by waterhouse 5188 days ago | link

What you describe is doable with closures, at least as far as your example goes. Your example can be implemented almost word-for-word in Arc as it stands:

  (def Socket (url port)
    (let o (obj on-receive (fn (..) ..)
                send (fn (..) ..))
      (fn (method-name . args)
        (apply (o method-name) args))))
obj is a macro that creates a hash table (you don't need to quote the keys, by the way). So, (Socket url port) will return a function that accepts 1 or more arguments, assumes the first is a method name, looks it up in the hash-table o, finds the corresponding function, and applies it to the rest of the arguments.

If you want to give your object thing internal state that you'll want to modify, use a let.

Here is an example that probably demonstrates most of what you're looking for. It is pretty much cribbed from SICP[0].

  (def Bank-Account (password)
    (let money 0
      (let check-pass [unless (is _ password)
                        (err "Wrong password!")]
        (let o (obj deposit (fn (x pw)
                              (check-pass pw)
                              (++ money x))
                    withdraw (fn (x pw)
                               (check-pass pw)
                               (if (< money x)
                                   (err "Not enough money.")
                                   (-- money x)))
                    check (fn (pw)
                            (check-pass pw)
                            money)
                    change-pw (fn (new-pw pw)
                                (check-pass pw)
                                (= password new-pw)))
          (fn (method-name . args)
            (apply (o method-name) args))))))

  ; In action
  arc> (= x (Bank-Account "meh"))
  #<procedure: Bank-Account>
  arc> (x 'deposit 50 "meh")
  50
  arc> (x 'check "meh")
  50
  arc> (x 'deposit 200 "meh")
  250
  arc> (x 'withdraw 300 "meh")
  Error: "Not enough money."
  arc> (x 'check "meh")
  250
  arc> (x 'deposit 20 "achtung")
  Error: "Wrong password!"
  arc> (x 'check "meh")
  250
  arc> (x 'change-pw "achtung" "meh")
  "achtung"
  arc> (x 'deposit 400 "achtung")
  650
[0]http://mitpress.mit.edu/sicp/full-text/book/book-Z-H-20.html...

-----

1 point by d0m 5187 days ago | link

Exactly, Arc already have OO with closures. However, what I suggest is this:

  (x 'deposit 400 "achtung")
could be instead written as:

  (deposit x 400 "achtung")
So basically, we get rid of the quote in front of deposit and we reverse the first and second parameter.

I find that:

  (let x (Bank-Account "meh")
    (deposit x 50)
    (check x))
Is cleaner then:

  (let x (Bank-Account "meh")
    (x 'deposit 50)
    (x 'check))

-----

3 points by waterhouse 5185 days ago | link

Given the way I defined a Bank-Account, you could implement this rather simply:

  (def deposit (x . args)
    (apply x 'deposit args))
Or, with akkartik's implementation, which I agree is nicer and better:

  (def deposit (x . args)
    (apply x!deposit args))
And you could make another "object" and give it a distinct 'deposit "method" and it would work fine.

So... it seems that one can easily implement these things in Arc. If you have some project in mind for which you want OO stuff, you can just do the things posted in this thread, and tell everyone how it goes if you like.

If you think OO should be put into the core of the language, well, there's a certain resistance to adding features just because someone suggests it. The core would become large, disorganized, and difficult to maintain if we did that. So, you would have to give a convincing argument as to why it's worth putting in there, and a demonstration of how useful it is in some real project of yours is probably the best argument you could give.

-----

3 points by akkartik 5185 days ago | link

Yes that's implementable; I find this even more expressive:

  (x!deposit 50 "meh")
The implementation is simpler than waterhouse's version - just return the obj:

  (def Bank-Account (password)
    (let money 0
      (let check-pass [unless (is _ password)
                        (err "Wrong password!")]
         (obj deposit (fn (x pw)
                        (check-pass pw)
                        (++ money x))
              withdraw (fn (x pw)
                         (check-pass pw)
                         (if (< money x)
                             (err "Not enough money.")
                             (-- money x)))
              check (fn (pw)
                      (check-pass pw)
                      money)
              change-pw (fn (new-pw pw)
                          (check-pass pw)
                          (= password new-pw))))))

-----

3 points by bogomipz 5172 days ago | link

This kind of OO with closures is a fun experiment and looks very elegant at first sight. I love the (x!deposit 50 "meh") version for its simplicity, the use of ssyntax, and the fact that you can pass x!deposit around as a first class function. Thanks to macros, you can of course easily come up with a nice syntax for the definitions:

  (defclass Bank-Account (password)
    (money 0 debt 0)
    (def check-pass (pw)
      (unless (is pw password)
        (err "Wrong password!")))
    (def deposit (x pw)
      (self!check-pass pw)
      (++ money x))
    (def withdraw (x pw)
      (self!check-pass pw)
      (if (< money x)
          (err "Not enough money.")
          (-- money x)))
    (def check (pw)
      (self!check-pass pw)
      money)
    (def change-pw (new-pw pw)
      (self!check-pass pw)
      (= password new-pw)))
However, the approach has some issues in real life use. First, every bank account instance replicates the method table and so takes up more memory the more methods the class defines, and each method is a closure that takes up memory as well. Also, this hash table obviously needs to be built every time an instance is created. Another big problem that follows from the above is that when you add or redefine methods on the class, existing instances are left with the old implementation. And there is no way to implement inheritance here.

I guess it is possible to remedy most or all of those problems by sacrifying methods as closures and instead do:

  (= bank-account-mt
    (obj check-pass (fn (self o pw)
                      (unless (is o!pw pw)
                        (err "Wrong password!")))
         deposit (fn (self o x pw)
                   (self 'check-pass pw)
                   (++ o!money x))
         withdraw (fn (self o x pw)
                    (self 'check-pass pw)
                    (if (< o!money x)
                        (err "Not enough money.")
                        (-- o!money x)))
         check (fn (self o pw)
                 (self 'check-pass pw)
                 o!money)
         change-pw (fn (self o new-pw pw)
                     (self 'check-pass pw)
                     (= o!pw new-pw))))

  (def Bank-Account (password)
    (let o (obj money 0 pw password)
      (afn (method-name . args)
        (apply (bank-account-mt method-name)
               (cons self (cons o args))))))
Again using a macro to improve readability and writability. Adding inheritance is left as an exercise for the reader.

-----

2 points by rocketnia 5172 days ago | link

I'm sure this doesn't surprise you, but here's a quick version of 'defclass that uses a syntax similar to your first example and an implementation similar to your second example:

  (mac defclass (name constructed-fields derived-fields . defs)
    (let mt (sym:string name '-mt)
      `(do (= ,mt (obj ,@(mappend
                           [do (case car._
                                 def  (let (name parms . body) cdr._
                                        `(,name (fn ,(cons 'self
                                                       (cons 'o parms))
                                                  ,@body)))
                                  (err:+ "An invalid 'defclass "
                                         "declaration was "
                                         "encountered."))]
                           defs)))
           (def ,name ,constructed-fields
             (let o (withs ,derived-fields
                      (obj ,@(mappend [list _ _]
                               (join constructed-fields
                                     (map car pair.derived-fields)))))
               (afn (method-name)
                 (fn args
                   (apply (,mt method-name)
                          (cons self (cons o args))))))))))
  
  (defclass Bank-Account (password)
    (money 0)
    (def check-pass (pw)
      (unless (is pw o!password)
        (err "Wrong password!")))
    (def deposit (x pw)
      self!check-pass.pw
      (++ money x))
    (def withdraw (x pw)
      self!check-pass.pw
      (when (< o!money x)
        (err "Not enough money."))
      (-- o!money x))
    (def check (pw)
      self!check-pass.pw
      o!money)
    (def change-pw (new-pw pw)
      self!check-pass.pw
      (= o!password new-pw)))

-----

1 point by bogomipz 5171 days ago | link

Nice, and you even changed it so x!deposit returns a function again! This does of course add some overhead since a closure is constructed every time you call a method, but still.

One thing I'm not quite happy with is that one has to write o!money. Would it somehow be possible to hide the o? Would it be possible to use !money or .money, or does the parser not allow that? And how to pass the hash table from the afn to the methods without polluting their namespaces? It could be done using a gensym, but then it is not possible to add methods to the method table outside defclass.

Perhaps doing something like this:

  (= bank-account-mt
    (obj check-pass (fn (self pw)
                      (unless (is self!ivars!pw pw)
                        (err "Wrong password!")))
         deposit (fn (self x pw)
                   self!check-pass.pw
                   (++ self!ivars!money x))
         withdraw (fn (self x pw)
                    self!check-pass.pw
                    (if (< self!ivars!money x)
                        (err "Not enough money.")
                        (-- self!ivars!money x)))
         check (fn (self pw)
                 self!check-pass.pw
                 self!ivars!money)
         change-pw (fn (self new-pw pw)
                     self!check-pass.pw
                     (= self!ivars!pw new-pw))))

  (def bank-account (password)
    (let ivars (obj money 0 pw password)
      (afn (selector)
        (if (is selector 'ivars)
            ivars
            (fn args
              (apply (bank-account-mt selector)
                     (cons self args)))))))
Then make defclass turn .foo into self!ivars!foo. Another macro could exist for (re)defining methods after the fact:

  (defmethod bank-account steal-money (x)
    (-- .money x))
Or even redefine Arc's def so you could do:

  (def bank-account!steal-money (x)
    (-- .money x))
since (bank-account 'steal-money) is not an atom and 'def could thus recognize it as different from an ordinary function definition.

-----