Arc Forumnew | comments | leaders | submitlogin
Arubic message passing: now as a library
3 points by Pauan 4964 days ago | 55 comments
I realized recently that it is possible to implement message passing ala Arubic as a library. This has multiple advantages. For starters, the core can potentially be simpler, since it doesn't need to worry about such things, and objects using message passing can blend in seamlessly with non-message passing objects.

For instance, right now hash tables in Arc are, well, hash tables. Ordinarily, trying to implement something that is like a table is difficult, which is what message passing is trying to solve.

But in order to be really seamless, hash tables would also need to use message passing, and thus it would require 2 hash tables and about 5 functions per hash table... what a waste.

What would be better is if hash tables could stay exactly the same, yet still allow for message passing to mimic tables. In his essays, pg said that he was relieved that he only needed to add type, tag, and rep to the language, and OOP could be implemented as a library with those three basic functions.

At first I thought that was impossible, that something like Arubic would need to be built into the core, but now I realize that it is possible to get seamlessness[1] with those three functions, and in addition, nothing in the core needs to change, with one possible exception[2]

So, with that revelation, I created a simple library called "object.arc"[3]. How it works is pretty simple: it creates an (object) macro that is like (obj) except it's return value is annotated with 'object:

  (object a 1 b 2) -> #(tagged object #hash((b . 2) (a . 1)))
  (type (object))  -> table
What's that? It has a type of 'table? That's so objects can work with things like `each` without needing to extend them. You can override that by giving the object a `type` attribute.

Oh, but now you can't tell whether it's a hash table or not... hm... wait, I know! You can use `object?` which can tell the difference:

  (object? (object)) -> t
I also provide some functions for manipulating objects: get-attribute, set-attribute, and del-attribute[4]. The names are cumbersome, but with proper ssyntax support, they could be very short.

I then extend various low-level functions like sref, maptable, and type. Then I extend coerce to give it custom behavior when called in functional position. Now you can grab the keys of an object (just like a hash table), and also assign to it:

  (= foo (object a 1))
  
  (foo 'a)   -> 1
  (keys foo) -> (a)
  
  (= (foo 'b) "qux")
  (keys foo) -> (a b)
Okay, big deal, hash tables can do that too. Now's when we get to the fun stuff. You can override those default behaviors just by giving the object appropriate attributes. For instance, let's say you want the object to do something when called:

  (= foo (object call (fn args
                        (prn "called with " args))))

  (foo 'a 'b 'c) -> called with (a b c)
Or to do something when assigned to:

  (= foo (object set (fn (k v)
                       (prn "assigning " k " to " v))))

  (= (foo 'a) "bar") -> assigning a to bar
There's also `keys`, and I plan to add in `del` later. You're free to add your own, of course.

Lastly, there's a `print` attribute, which lets you customize how your object is displayed:

  (object print (fn () "%custom printing!%")) -> %custom printing!%
---

* [1]: I have found one caveat: my implementation extends `type`, so if you load the "object.arc" library two or more times (in the same namespace), it will go into an infinite loop. But as long as you load it only once, it should be fine. I might be able to fix that.

* [2]: If your favorite implementation of Arc supports something similar to defcall, then you can implement message passing. If your favorite implementation does not support defcall, then you most likely won't be able to.

* [3]: ar does not provide something with the power of defcall, but my fork does. You can find my fork at https://github.com/Pauan/ar

The "object.arc" library is in the "lib/" subdirectory.

* [4]: Why del-attribute? Because unlike hash tables (in Arc), objects can have nil as both a key and a value, which is different from a lack of value:

  (= foo (object a nil))

  (foo 'a (uniq)) -> nil
  (foo 'b (uniq)) -> g459
Oh yeah, and if you ever want a hash table to contain nil, you can use get/set/del-attribute on hash tables too:

  (= foo (table))

  (set-attribute foo nil 'a)
  (set-attribute foo 'a nil)

  foo -> #hash((nil . a) (a . nil))


1 point by Pauan 4962 days ago | link

https://github.com/Pauan/ar/commit/d1888d145ca5c8c651d526b54...

There's now an implicit variable "self" that all objects have:

  (object call  (fn (x) (+ x self<-value))
          value 5)
No more need for letr, yay!

And ssyntax support is in, so you can say foo<-bar rather than (get-attribute foo 'bar)

I also added in del, which can delete object attributes and hash keys:

  (del foo<-bar)
  (del (foo 'bar))
I plan to add in more stuff to del later, like (del (car foo)) and such.

Oh yeah, and isa now understands multi-type objects. For instance:

  (= foo (object type '(cons table)))

  (type foo)       -> (cons table)
  (isa foo 'cons)  -> t
  (isa foo 'table) -> t
It's not perfect, though. If a function uses `type`, then they'll get the raw list, but as long as they use `isa`, then it should be okay. I might actually be able to fix that... but it'll probably require extending `is`.

---

By the way, since "self" is dynamic, you can grab a method from a different object and use it with your object:

  (= foo (object x (fn () ... self ...)))
  (= bar (object x foo<-x))
And that means functions don't need a special "self" argument, so normal functions work too. You can also parameterize it, to call one object's methods on a different object:

  (w/self foo
    (bar<-x ...))
This is like Function.prototype.call/apply in JS:

  bar.x.call(foo, ...)
But lexical variables work fine too, so if you want "self" to always refer to your object, and never to anything else, you can use something like letr:

  (letr self (object ...) self)
And due to the power of lexical scope + closures, w/self won't work anymore. Naturally you should only use this technique when you reaaaally don't want people to use w/self to change the self binding. Not only is it more verbose, but it's less flexible and hackable.

---

I haven't implemented it yet, but there was one more thing I wanted to add: Python style methods. Having self be implicit is really great, but what if you want to use a name other than self? To solve this problem, I plan to have a "method" macro, which creates a function that accepts a "self" argument:

  (method (self a b c)
    ...)
Which is basically equivalent to this:

  (fn (a b c)
    (let self self
      ...))
So now, you can change the name of that argument to anything you want, and "self" will stay dynamic. An example where this would be useful is a method that calls afn, since afn also preselects the name "self" for you:

  (object print (method (this)
                  (afn () ...)))
Now, you can use "this" to refer to the object, and "self" to refer to the afn function. Once I have that implemented, I'll have all 3 bases covered: lexical, dynamic, and Python-style.

-----

1 point by Pauan 4961 days ago | link

"It's not perfect, though. If a function uses `type`, then they'll get the raw list, but as long as they use `isa`, then it should be okay. I might actually be able to fix that... but it'll probably require extending `is`."

https://github.com/Pauan/ar/commit/4243628823fd8c66bbedcbe05...

Fixed. Now objects can have an `is` attribute that lets them determine equality to other things:

  (= foo (object is (fn (x)
                      (in x 'bar 'qux))))

  (is foo 'bar)   -> t
  (is foo 'qux)   -> t
  (is foo 'corge) -> nil
Now we're starting to get to the really good stuff. Try doing that easily without objects. :P So now multi-types work perfectly:

  (= foo (object type '(cons table)))

  (case (type foo)
    cons  1
    table 2)            -> 1
  
  (is (type foo) 'cons) -> t

  (type foo)            -> #<is [cons table]>
  (isa foo 'cons)       -> t
  (isa foo 'table)      -> t
And if you ever find it useful, there's a low-level function called `make-multi-is` that accepts a sequence to test for equality:

  (= foo (make-multi-is '(cons table)))

  (is foo 'cons)  -> t
  (is foo 'table) -> t
  (is foo 'bar)   -> nil

-----

2 points by rocketnia 4961 days ago | link

  (is foo 'cons)  -> t
  (is foo 'table) -> t
Ick. I like 'is being an equivalence relation. Is there a way you could achieve what you want with 'testify instead?

I'm thinking something like this:

  (def isa (x typ)
    (.typ:testify type.x))

  (extend testify (x) alist.x
    [some _ x])                ; uses testify on '_ too!
This doesn't work on (case type.foo ...) though. In 'case, I'd prefer to be able to use predicates as the cases, so what I really want is for the cases to be testify'd. On the other hand, I still believe techniques like 'isa and (case type.foo ...) limit extensibility (even if you're trying to lift those limits a bit here ^_^ ), so I'm not married to any particular semantics for them.

-----

1 point by Pauan 4961 days ago | link

"Ick. I like 'is being an equivalence relation. Is there a way you could achieve what you want with 'testify instead?"

Nope. I don't see a problem with overriding `is`, if that's what you want to do. If you don't want to override `is`, then just don't. Hell, you could override `is` and use testify, if you wanted to.

The point of the `is` attribute is that your object might have a custom idea of equivalence relationship. It's up to you, the designer of the object, to use a sane definition of "equivalence".

Now, if you had suggested to use `iso` instead of `is`, then I could be down with that, but then things like `isa` would need to be changed so they used `iso`... I'm not against the idea, but it would cause incompatibility with Arc/3.1.

I'll also note that the concept of something being eq to multiple things is not exactly new:

  (is nil ())
  (is nil '())
  (is nil 'nil)
So I think you should be allowed to define a custom `is` behavior, when it makes sense for your object.

---

By the way, originally I extended `isa`, which did work fine, as long as you used `isa` only, but I really wanted it to be perfectly seamless. And, by doing this, I also get another benefit: objects can have a type of object and table, so (isa foo 'object) works:

  (= foo (object))

  (isa foo 'table)  -> t
  (isa foo 'object) -> t
I would still recommend using `object?` to test for object-ness, though. In addition, if I did this change, then I think I can fix the bug where you can't load the library more than once in the same namespace.

-----

1 point by rocketnia 4961 days ago | link

"It's up to you , the designer of the object, to use a sane definition of "equivalence"."

I agree, but your extension to 'is already violates the mathematical definition of an equivalence relation, so it's already insane as far as I'm concerned. For 'is to be an equivalence relation, if (is foo 'cons) and (is foo 'table), then anything equivalent to 'cons must be equivalent to 'table, and that renders the type checks rather useless.

---

"Now, if you had suggested to use `iso` instead of `is`, then I could be down with that, but then things like `isa` would need to be changed so they used `iso`... I'm not against the idea, but it would cause incompatibility with Arc/3.1."

Yes, I consider the pervasive use of 'is to be a flaw of Arc. I'm happy Wart doesn't approximate Arc in this way, and I don't design my languages/libraries to approximate Arc in this way either.

---

"(is nil ())"

When I use 'is, I'm asking if there's any possible way for me to distinguish the values. IMO, all ways to distinguish nil from () in Arc are "nonstandard," so (is nil ()) is just fine.

If there were a way to distinguish them, I wouldn't want (is nil ()) to be true. We could still have (iso nil ()), since 'iso can still be an equivalence relation with that constraint. (Everything 'iso to nil would be 'iso to () as well.)

-----

1 point by Pauan 4961 days ago | link

"...then anything equivalent to 'cons must be equivalent to 'table, and that renders the type checks rather useless."

Right, so if your object has a type of both cons and table, it should definitely act like both a cons and a table, meaning it should have a car, cdr, etc.

---

"Yes, I consider the pervasive use of 'is to be a flaw of Arc. I'm happy Wart doesn't approximate Arc in this way, and I don't design my languages/libraries to approximate Arc in this way either.

[...]

If there were a way to distinguish them, I wouldn't want (is nil ()) to be true. We could still have (iso nil ()), since 'iso can still be an equivalence relation with that constraint. (Everything 'iso to nil would be 'iso to () as well.)"

Alrighty then, it looks like we have a bit of a viewpoint clash. You're seeing `is` as being like `eq`: super-strict, checking for exact equivalence. That's totally fine. I'm seeing `is` as being similar to, but a little bit looser than `eq`.

So perhaps what would be nice is if Arc had 3 operators: eq, is, and iso. eq is stricter than is, which is stricter than iso. Or perhaps we could just get rid of `is` completely and just use `eq` and `iso`.

In any case, those are incompatible changes, so given aw's position with ar, I'm not exactly planning to change ar in that way, even on my fork. Now, if we're talking about Arubic, then yes, perhaps Arubic could be designed differently, using iso everywhere rather than is.

But in the context of ar, I think overloading `is` in this way is the most reasonable choice, given how much of Arc uses `is`. Remember: my goal with this library is to allow you to easily and concisely create both new data types, and also data types that resemble existing data types, like tables.

Thus, I want objects to be as seamless as possible with the existing language. Unfortunately, that means that if the existing language made some poor choices, my library might end up doing something that's kludgy or hacky to make it work. Or hell, maybe the language is perfectly fine, and my library is just hacky and kludgy. :P

In any case, I think an object library designed more in line with a perfect ideal Arc would probably be designed differently than mine, and I'm okay with that, since mine is designed to get things done within existing Arc code.

-----

2 points by rocketnia 4961 days ago | link

"Right, so if your object has a type of both cons and table, it should definitely act like both a cons and a table, meaning it should have a car, cdr, etc."

We're miscommunicating spectacularly today. XP

If any one thing is equivalent to both 'cons and 'table, then everything equivalent to 'cons must be equivalent to 'table (for us to be talking about an equivalence relation). This means that if anything's type 'is the symbol 'cons, then its type 'is also 'table. All conses are tables. The converse holds too: All tables are conses.

This is absurd (in a non-mathematical way ^_^ ), so we shouldn't let it get that far: Either you must believe that 'is is not an equivalence relation, or you must accept you're not using 'is propahlay. Anyway, I think you are consciously resigning yourself to one or both of those things, in order to "get things done within existing Arc code."

Personally, I'm very worried Arc-compatibility in ar will hold it back in ways like this.... I'm going to do what I can to help establish compatibility anyway though.

---

"So perhaps what would be nice is if Arc had 3 operators: eq, is, and iso."

Aahhhh, no way. XD I think Akkartik says it pretty well at http://arclanguage.org/item?id=13690: "But the problem isn't that it's impossible to find the one true semantic for equality or coercion, it's that the language designers didn't put their foot down and pick one. Don't give me two different names with subtle variations, give me one strong name."

This helps because there's just one utility for people to use and extend, not a variety of utilities which compete for attention. And as I say at http://arclanguage.org/item?id=12803, if we need a different notion of equality, we can define a wrapper type with its own 'iso support which compares its contents using that new notion.

But in ar, maybe you're right. From http://arclanguage.org/item?id=248, it's clear that your lax interpretation of 'is is consistent with pg's intentions for it. Perhaps ar should provide 'eq and each Arc variant running on ar should choose its own notion of equality.

-----

1 point by Pauan 4961 days ago | link

"All conses are tables. The converse holds too: All tables are conses."

Woah, hey, that gives me an idea... :P

---

"This is absurd (in a non-mathematical way ^_^ ), so we shouldn't let it get that far:"

You sure about that? :P

In all seriousness, I considered representing tables in Arubic as alists (or possibly plists). But that's getting off-topic...

---

"This helps because there's just one utility for people to use and extend, not a variety of utilities which compete for attention."

Absolutely. So I went in and extended `is`. But now you're saying that extending `is` is bad because you want `is` to behave like `eq`. :P It was part-serious and part-joking, since although it would solve this particular problem, I can understand your reluctance to add in a zillion equality-testing things.

---

"Perhaps ar should provide 'eq and each Arc variant running on ar should choose its own notion of equality."

Sounds good to me, provided that I can use Arc's `is` when I want to. In other words: ar provides eq, Arc/ar provides is, and Arc variants can choose to use either one, both, or neither.

---

"Personally, I'm very worried Arc-compatibility in ar will hold it back in ways like this...."

Me too. Personally, I want to just go in and rip up things, and have a backcompat.arc file that smooths over things, but alas... Hell, that's what I did on my fork, and backcompat.arc is loaded automatically, so it behaves almost exactly[1] like Arc/3.1.

---

* [1]: The primary difference is when you want to extend something. For instance, let's say you wanted to extend "no". But my fork doesn't use "no", it uses "not", so you'd be extending the wrong thing. Stuff like that. Probably possible to work around, but ugh.

So, if I can get a perfect compatibility layer, then sure, let's go with that, but as long as it's imperfect, then there will always be some niggling corner case which could be used as justification for not changing ar.

-----

1 point by rocketnia 4961 days ago | link

"You sure about that? :P"

As I'm sure you're aware, conses and tables are an arbitrary part of the example. ^_^ If you wanted anything to act as both a 'num and a 'cons, then all conses would be nums too.

---

"Me too. Personally, I want to just go in and rip up things[...]"

Yeah... I wonder if I should be forking your ar instead of working on aw's. >.> It would become a fork of a fork of a fork of an Arc. :-p Maybe since it's hard to pull-request all those changes, you could treat your ar as a separate project?

Eh, I'm not usually one to make an Arc spinoff from existing source anyway. I'm more likely to either give advice from the sidelines or start from Racket (or JavaScript) and implement utility layers on top of it until I have a system I like. So don't worry about my participation either way, heh. ^_^;

-----

1 point by Pauan 4961 days ago | link

"As I'm sure you're aware, conses and tables are an arbitrary part of the example. ^_^ If you wanted anything to act as both a 'num and a 'cons, then all conses would be nums too."

Of course, I was joking. Wait a second... we could Church encode numbers... then they really would be conses...! (I wonder how much longer I could keep this up :P)

---

"Maybe since it's hard to pull-request all those changes, you could treat your ar as a separate project?"

Yeah, I'm kinda leaning toward that, especially since some of my changes break backwards compatibility, and aw has made it pretty clear that Arc/ar is aiming at compatibility.

-----

2 points by rocketnia 4961 days ago | link

"I wonder how much longer I could keep this up :P"

Probably until we reach "Everything's an object." Then we can take a look at all the ways we distinguish certain objects from others, and we can start all over again. ^_^

-----

2 points by rocketnia 4961 days ago | link

"I plan to add in more stuff to del later, like (del (car foo)) and such."

What would that do? Would it just be the same as (wipe car.foo), or what?

---

"(w/self foo (bar<-x ...))"

Then how do you usually invoke a method on an object? I guess what I mean is, what's the shortcut for (w/self bar (bar<-x ...))?

-----

1 point by Pauan 4961 days ago | link

"What would that do? Would it just be the same as (wipe car.foo), or what?"

It would be equivalent to:

  (zap cdr foo)
And (del (cdr foo)) would be:

  (scdr foo nil)
Thus:

  (= foo '(1 2 3 4))

  (del (car foo)) -> (2 3 4)

  (del (cdr foo)) -> (2)
And it should be able to work on indices as well:

  (= foo '(1 2 3 4))

  (del (foo 2)) -> (1 2 4)
And it would be awesome if it worked on ranges too:

  (= foo '(1 2 3 4 5 6 7))

  (del (foo 1 4)) -> (1 6 7)
---

"Then how do you usually invoke a method on an object? I guess what I mean is, what's the shortcut for (w/self bar (bar<-x ...))?"

No shortcut yet, but the idea is that hopefully you won't need to most of the time. The point of defining methods like `call` is that you don't call them directly:

  (= foo (object call (fn () ...)))

  (foo ...)
A shortcut for it would be nice, though.

One idea would be that get-attribute would return a binded form, so you could just call (foo<-x ...) directly, without needing w/self, but still allow w/self to override it.

-----

1 point by rocketnia 4961 days ago | link

"(zap cdr foo)"

What's the advantage 'del gives me? Wouldn't my programs have fewer tokens if I directly used (zap cdr foo) instead of (del car.foo)?

Right now 'del looks like a smorgasbord of special-case utilities which could all have shorter tailor-made alternatives. Ideally what I like to see is a utility with an abstract enough description that I can build new tools upon it and straightforwardly extend it to make all my tools handle a new case. (Basically I have the same indifference to 'del as I have to 'coerce. ^_^ )

---

"The point of defining methods like `call` is that you don't call them directly"

Okay, I thought it might be something like that. ^_^ But in that case, it reminds me of Racket's structure type properties again. It's up to you to decide whether that's a flattering comparison or not, but you didn't seem to like them the last time.

---

"One idea would be that get-attribute would return a binded form, so you could just call (foo<-x ...) directly, without needing w/self[...]"

That's what I was intuitively thinking would happen. ^_^ I usually wouldn't say that, though. Perhaps I've been working in JavaScript for too long now. :-p

---

"[...]but still allow w/self to override it."

Er, even my currently weird intuition doesn't like that kind of inconsistency. >.>

What about making 'get-attribute a metafn? That is, ((get-attribute a b) ...) would expand into something like (invoke-attribute a b ...) the same way ((compose a b c) ...) expands into (a (b (c ...))).

Then when you want to call foo's x from bar, you say something like this:

  (w/self bar
    ((do foo<-x) ...))
The (do ...) would keep (get-attribute foo x) from being in functional position, so it wouldn't be treated as a metafn.

This would take a bit of language-core-hacking. Metafns are special-cased like special forms in official Arc--and even in your fork of ar, even though you're using gensyms for special forms. I would have tried to "fix" that in Arc a long time ago, but my new design for custom syntax was so different from Arc's that I considered it part of Penknife instead. The most straightforward way to get started in ar would probably be to design metafns as macros which expanded into literal macros, and then to let literal macros expand even when they're not in functional position.

---

At this point these are all just substanceless, brainstorming-like suggestions. I won't mind too much if you throw them all out, just as long as you've at least thought about them. ^_^

-----

1 point by Pauan 4961 days ago | link

"What's the advantage 'del gives me? Wouldn't my programs have fewer tokens if I directly used (zap cdr foo) instead of (del car.foo)?"

Abstraction and readability. Let me answer your question with a question: why do we need (= (car foo) ...)? you can just use scar! Or why do we need (= (foo 'a) ...)? We can just use things like sref!

For the same reasons that overloading = to understand things is nice, overloading del to understand things is nice. So, if you feel that using (scar foo ...) is better than (= (car foo) ...), then by all means use (zap cdr foo), but I personally like using the = forms, and so I like del for the same reasons.

I'll note that there is a defdel, which lets you create new "del" stuff, similar to defset. And you can extend dref as well, just like extending sref. It's basically the exact same idea as =, except it's applying to deletion rather than assignment.

And let me say one more thing. How would you easily and concisely implement this, which deletes a range of elements:

  (= foo '(1 2 3 4))

  (del (foo 1 2)) -> (1 4)
Or how about doing it in steps, so you could delete, say, every other element in a list:

  (= foo '(1 2 3 4 5 6 7))

  (del (foo 0 nil 2)) -> (1 3 5 7)
Sure, you could write a special-case function for that, in the same way we have special-case functions like scar, scdr, and sref, but it's nice to be able to bundle them up into a single convenient macro, like del.

---

"It's up to you to decide whether that's a flattering comparison or not, but you didn't seem to like them the last time."

I didn't like how they automatically created stuff like exn-message, rather than letting you do (foo 'message). That's different from disliking the overall idea.

---

"Perhaps I've been working in JavaScript for too long now. :-p"

JavaScript does have bind... :P

  foo.bind(this)
---

"This would take a bit of language-core-hacking."

Or I could just use dynamic variables, which is what I'm doing. It's quite simple: if self is bound, use that, otherwise use the object. Basically, self defaults to the current object, while letting you override it with w/self.

This is actually very similar to JavaScript:

  foo.method(...)
  foo.method.call(bar, ...)
Which would be like this, with my library:

  (foo<-method ...)
  (w/self bar (foo<-method ...))
The difference is, that in JS they use the read-only "this" keyword, but in my library I'm just using an ordinary dynamic variable.

-----

1 point by rocketnia 4961 days ago | link

"Let me answer your question with a question: why do we need (= (car foo) ...)? you can just use scar! Or why do we need (= (foo 'a) ...)? We can just use things like hash-set!"

I was originally going to use that as an example. ^_^

Arc's notion of assignment follows the abstract, informal contract that (= (foo ...) bar) changes whatever (foo ...) returns. This means we can have utilities like 'zap and 'swap that treat something as both a "get"-like expression and a "set"-like expression. I don't see 'del having a contract like that, so I don't see why "it's nice to be able to bundle them up."

I'd implement your range deletion examples like this...

  (mac del (place start (o howmany 1) (o step 1))
    ...)
...and say (del foo 1 2) and (del foo 0 nil 2).

---

"I didn't like how they automatically created stuff like exn-message, rather than letting you do (foo 'message). That's different from disliking the overall idea."

Ah, okay. That works for me.

---

"JavaScript does have bind... :P"

I'm already fully aware a JavaScript programmer can control the value of "this" using call, apply, or bind. ^_^

All I'm talking about is the way that foo.bar( baz ) calls the value of foo.bar with foo as "this," but if you instead define idfn() and say idfn( foo.bar )( baz ), then "this" is the root object. That's not the most elegant semantics in the world, but I'm saying my intuition is somehow in line with it right now.

(The "root object" aspect isn't part of my intuition, though. I'm fine with idfn( foo.bar )( baz ) using anything for "this", including treating it as a dynamic variable like you are.)

---

"Or I could just use dynamic variables, which is what I'm doing."

Again, I was already taking that for granted and not arguing with it. Instead, I was talking about a way for (foo<-x ...) to automatically use (w/self foo ...), the way JavaScript foo.x( ... ) is like foo.x.call( foo, ... ).

---

"It's quite simple: if self is bound, use that, otherwise use the object."

That's not reassuring....

I'm particularly concerned about the case where we're using (foo<-method ...) from inside another object's method. I'm afraid the self variable will already be bound then, so instead of calling the method on foo, we'll end up calling it on the current object. As a workaround, we'll have to say (w/self foo (foo<-method ...)).

I guess we could probably say (w/self nil (foo<-method ...)) too, but that's still not ideal.

-----

1 point by Pauan 4961 days ago | link

"Arc's notion of assignment follows the abstract, informal contract that (= (foo ...) bar) changes whatever (foo ...) returns."

del's contract is that it zaps the variable to whatever dref returns. It is more analogous to = than you think. In fact, it's exactly the same idea, only the name is different, and it's purpose is different.

So, my point still stands. If you think that (= (car foo) ...) is good, then why is (del (car foo)) bad? You may think, "well, I don't think that's useful". Fine. There's plenty of Arc functions I don't think are useful. Just don't use del, then.

But in the same way that = being general-purpose is obviously useful, del being general purpose is obviously useful. For instance, how do you delete an attribute?

  (del foo<-bar)
Compared to:

  (del-attribute foo 'bar)
How do you define a custom deletion behavior for your object? Just slap on a `del` attribute, in exactly the way that objects can define a `set` attribute.

Did you define new functions "get-foo", "set-foo" and "del-foo"? Well, nobody has to know about "del-foo" because you can just use defdel, in the same way that nobody needs to know about scar, because you can just use (= (car ...) ...)

And nobody has to know about set-foo, because you can use defset... and nobody has to know about get-foo, because you can use defcall. Notice the pattern, here?

---

"The specific case I'm interested in is the one where we're using (foo<-method ...) from inside another object's method. I'm afraid the self variable will already be bound then, so instead of calling the method on foo, we'll end up calling it on the current object."

Ah yes, good point. I should have a unit test for that.

-----

1 point by akkartik 4961 days ago | link

"It is more analogous to = than you think. In fact, it's exactly the same idea, only the name is different, and it's purpose is different."

If the name and purpose are different what's left? :p

Slices and steps don't currently work with '=. So they feel bolted on to del.

There's two separate issues here: whether we need del in addition to '=, and whether places should support slices and steps. Let's discuss them separately.

-----

1 point by Pauan 4961 days ago | link

"There's two separate issues here: whether we need del in addition to '=, and whether places should support slices and steps. Let's discuss them separately."

Of course. I don't recall ever conflating the two, and I think we should allow for both assigning to slices, and deleting slices. In fact, then deletion could use assignment, so this:

  (del (foo 0 1))
Would expand to this:

  (= (foo 0 1) nil)

-----

1 point by akkartik 4961 days ago | link

  (= (foo 0 1) nil)
Incidentally, I just remembered that there's already a name for assigning to nil: wipe. May still make sense to have both names, though.

-----

1 point by Pauan 4961 days ago | link

Yes, except we would need to change sref so it understands slices. Which I think we should do.

-----

2 points by rocketnia 4961 days ago | link

I don't. We already have (= (foo 0) ...) modifying the first element, so we don't have room for it to modify the slice 0..<(0 + 1). We already have (zap inc (my-table idx 0)) incrementing my-table.idx with a default starting value of 0, so we don't have room for it to modify the slice idx..<(idx + 0).

(FYI: The ..< I'm using is Groovy's notation for half-inclusive ranges, inspired by Ruby's ... notation but less ambiguous with Groovy's other syntax. I also prefer it for its obvious asymmetry.)

It would probably make more sense to use (slice foo ...) to get a slice and (= (slice foo ...) ...) to put it back.

This isn't ideal, as far as I'm concerned, 'cause it means we don't always get what we put in:

  arc> (= foo '(a b c d e f g))
  (a b c d e f g)
  arc> (= (slice foo 1 3) '(x y))
  (x y)
  arc> foo
  (a x y e f g)
  arc> (slice foo 1 3)
  (x y e)
However, that's not really part of the existing contract:

  arc> (= foo (table))
  #hash()
  arc> (= (foo 'a 1) nil)
  nil
  arc> foo
  #hash()
  arc> (foo 'a 1)
  1
Furthermore, I don't know a place I'd use that contract even if it did exist. ^_^ So I retract my objections for now, just as long as slice-getting isn't ambiguous.

-----

1 point by Pauan 4961 days ago | link

How would it be ambiguous? (foo 0 1) returns a list of the first element. (foo 0 nil) returns the entire list, and those would work in assignment too:

  (= (foo 0 1)   '(3 4))   ; splice a list into the first element
  (= (foo 0 nil) '(1 2 3)) ; replace the list with a different one
Hm... why not use cut rather than slice?

  (= (cut foo 0 1) '(3 4))
I mean, cut already exists, so...

-----

2 points by rocketnia 4961 days ago | link

"(foo 0 1) returns a list of the first element."

In tables, (foo 0 1) already means the same thing as (or foo.0 1). But hmm, maybe you're not interested in slicing tables. That's fine, then. ^_^

By the way, newLISP does something like that with (0 1 foo): http://www.newlisp.org/downloads/newlisp_manual.html#implici...

The other issue was that I think at some point you were using (foo 0) to mean a one-element slice, and that would definitely be ambiguous with regular old indexing.

---

"Hm... why not use cut rather than slice?"

I was going to say that, but I didn't want to clutter my point too much. Arc's 'cut accepts start and stop positions, and your slicing examples instead use a start position and a length. Translating between those was making my post harder to follow (I think), so I just assumed the definition of 'slice instead. Either should work. ^_^

-----

1 point by Pauan 4961 days ago | link

"In tables, (foo 0 1) already means the same thing as (or foo.0 1). But hmm, maybe you're not interested in slicing tables. That's fine, then. ^_^"

What would a table slice even mean? They're not even ordered. The closest thing would be a table that has a subset of the keys of another table, but that wouldn't use numeric indexing.

---

"The other issue was that I think at some point you were using (foo 0) to mean a one-element slice, and that would definitely be ambiguous with regular old indexing."

I was? (foo 0) should always be an element, not a slice. Ah well, it doesn't matter. If I did use that, I was incorrect.

I'll note that (del (foo 0)) is equivalent to (= (foo 0 1) nil) so perhaps that confused you.

---

"Arc's 'cut accepts start and stop positions, and your slicing examples instead use a start position and a length."

They did? I always thought of it as being a start/end position, like cut. So for instance:

  (= foo '(1 2 3 4 5 6))

  (= (foo 2 5) nil)

  foo -> (1 2 6)
The way I'm doing it is, the number of elements is the end minus the start, so for instance, in the example above, 5 - 2 is 3, so it removed 3 elements, starting at position 2. This is exactly how cut behaves:

  (= foo '(1 2 3 4 5 6))

  (cut foo 2 5) -> (3 4 5)
In fact, I think it'd be a good idea for lists/strings in functional position to just delegate to cut, so (foo 2 5) would be the same as (cut foo 2 5)

-----

2 points by rocketnia 4961 days ago | link

"What would a table slice even mean ?"

Hence my "but hmm." ^_^

---

"I'll note that (del (foo 0)) is equivalent to (= (foo 0 1) nil) so perhaps that confused you."

Yeah, that's probably it.

---

"I always thought of it as being a start/end position, like cut."

It's hard to tell because you use 0 as the start pretty often, but here are the two examples I was going off of:

From http://arclanguage.org/item?id=14656:

  (= foo '(1 2 3 4 5 6 7))

  (del (foo 1 4)) -> (1 6 7)
From http://arclanguage.org/item?id=14660:

  (= foo '(1 2 3 4))

  (del (foo 1 2)) -> (1 4)
Guess those were buggy. Well, now I know. :-p

-----

1 point by Pauan 4961 days ago | link

"Guess those were buggy. Well, now I know. :-p"

Ah, yeah, I switched from inclusive to half-open. And since I just so happened to use 1 as the start index, you couldn't really tell.

-----

1 point by akkartik 4961 days ago | link

What would a table slice even mean?

A sub-table with just the given keys?

-----

1 point by Pauan 4961 days ago | link

Well, sure, but you wouldn't use numeric indices to refer to that. :P It might be something like this:

  (subset x 'foo 'bar 'qux)
As opposed to:

  (x 0 1)
Which would be slice notation, which would work only on ordered sequences, like lists.

Which isn't to say that table subsets would be a bad idea, just that it's a different idea from list slicing, so there shouldn't be any ambiguities with slice notation (which was one of rocketnia's concerns).

-----

1 point by akkartik 4961 days ago | link

You could implement subsetting at function position, in which case it would be ambiguous. I already use :default in wart, so I don't care too much about that :) But yeah I hadn't thought of how to create ranges of keys. This may not be a good idea. You can't distinguish table lookup from a subset table with just one key/val, for example.

-----

1 point by Pauan 4961 days ago | link

Well, we could define it so table slicing would only happen with 3 or more arguments:

  (foo 'a 'b 'c)
...but I would consider that to be confusing, and would suggest an explicit function like "slice" or "subset".

-----

1 point by akkartik 4961 days ago | link

Yeah that's my sole feedback on del at this point: put the smarts into sref and defcall.

I have no idea how to do that :)

-----

1 point by Pauan 4961 days ago | link

Well then, guess I'll go in and do it, as a library for starters, and then we can potentially get it into base ar later.

By the way, sref has a terrible argument signature:

  (sref foo value key)
Intuitively, it really should be[1]:

  (sref foo key value)
But that's a backwards-incompatible change... Also, setforms only sends a single argument to sref, so we would need to change that too. For instance:

  (= (foo 0 1) 'bar)
Is translated into:

  (sref foo 'bar 0)
But it should be:

  (sref foo 'bar 0 1)
But if I change that, it breaks other things. Ugh. This really wasn't designed with slices in mind.

---

* [1]: To be fair, having the key as the last argument is great for slices, but bad for pretty much everything else. The argument order would make a lot more sense if sref had been designed with slice support.

-----

1 point by rocketnia 4961 days ago | link

"Also, setforms only sends a single argument to sref, so we would need to change that too."

Yeah, I think I noticed that at one point a long time ago, and I consider it to be a bug. (I promptly forgot about it. XD;; ) I never rely on that behavior, but then I don't extend things in the Arc core much either, for cross-implementation compatibility's sake.

---

"To be fair, having the key as the last argument is great for slices"

I think that's the reason it is the way it is. I've ended up designing some of my utilities the same way. For instance, I usually give failcall the signature (func fail . args), which is very similar to sref's (func new-value . args). I'd probably call 'sref "setcall" if I were to reinvent it.

I'm not sure I want to reinvent 'sref though: It's kinda bad as a failcallable rulebook, because you can't tell if it will succeed until you've called it, and then you get side effects. I've been thinking about other designs, like a currying-style ((sref func args...) new-value) version or something. But this is kinda specific to a system that uses failcall. In Arc, we can't really recover when an extension doesn't exist, so we don't write code that tries yet.

-----

1 point by Pauan 4961 days ago | link

Oh! I just found a bug! In arc.arc:

  (= (sig 'msec nil))
Should be:

  (= (sig 'msec))
Right? And here too...

  (= (sig 'seconds nil))
Should be:

  (= (sig 'seconds))
Okay, this is too much of a coincidence. aw, is there a reason for this, or was it just a mistake that we can safely fix?

-----

1 point by akkartik 4961 days ago | link

"Right now 'del looks like a smorgasbord of special-case utilities which could all have shorter tailor-made alternatives."

That was my first reaction as well. I see one general new idea here, though: to extend indexing to return not just places but slices like in python and Go.

  (= l '(1 2 3 4))
  (l 1) ; 2
  (l 0 2) ; '(1 2)
  (= (l 0 2) '(3)) ; l is now '(3 3 4)
Now del becomes identical to setting a range to nil.

  (= l '(1 2 3 4))
  (= l.0 nil) ; l is now (nil 2 3 4)
  (= (l 0 1) nil) ; l is now (2 3 4)
Would this be useful?

-----

1 point by Pauan 4961 days ago | link

"Would this be useful?"

It is in Python. Would it be useful in Arc? We already have cut, which does the same thing:

  (cut '(1 2 3 4) 0 2) -> (1 2)
But having a shorter syntax is nice. Would it be useful to assign to it? I think so, for the same reasons that it's nice in Python.

-----

1 point by akkartik 4961 days ago | link

"It is [useful] in Python."

Python doesn't allow assigning to slices, I think.

-----

1 point by Pauan 4961 days ago | link

It absolutely does! It also allows for deleting indices/slices:

  foo = [1, 2, 3, 4]
  foo[0:2] = [0]
  foo -> [0, 3, 4]

  del foo[0:1]
  foo -> [3, 4]
In fact, there's an idiomatic way to delete every element in a list:

  del foo[:]
  foo -> []

-----

2 points by akkartik 4961 days ago | link

Ah.

And del is identical to assigning to [].

  >>> foo = [0, 1, 2]
  >>> foo[0:1] = []
  >>> foo
  [1, 2]
I stand corrected.

-----

2 points by akkartik 4963 days ago | link

How do I refer to other object attributes inside print?

-----

1 point by Pauan 4963 days ago | link

  (letr self (object ...)
    self)
If this pattern is common enough, it shouldn't be hard to make a macro for it. In fact, that'd be a good idea, to provide a w/object macro:

  (w/object self ...)

-----

1 point by Pauan 4963 days ago | link

https://github.com/Pauan/ar/commit/6ea1169af4fa08d37c2d0a35f...

You can now use w/object to refer to the object. If you want finer-grained control, use let, letr, or whatever else you would normally use.

---

Question: should it be like afn and preselect the name "self" for you, or should it allow you to name it whatever you want? You can already use letr to give it a custom name, so I think it makes sense for w/object to preselect the name, like afn.

Of course, then it should probably be called "aobject" so it follows the same pattern as "afn" and "aif", but I think that looks funky. A better suggestion for naming conventions would be great.

Alternatively, I could have "object" automatically select the name "self", and then have a different macro that wouldn't name it. That's probably the best way, since it's really frequent to want to refer to the object from inside the object's methods.

---

Or I could go the Python route and make it so the first parameter of methods is "self":

  (object ... (fn (self ...) ...))
But I honestly don't like that, mostly because of the extra verbosity. I'd rather use "letr" to bind it once, rather than putting an additional argument into every single method... However, there are some nice benefits with this approach. Let's say you define a function:

  (def x (self) ...)
You can then slap it onto an object easily:

  (object x x)
This would also allow you to easily use methods from other objects:

  (= foo (object x (fn (self) ...)))
  (= bar (object x foo<-x))
And it would allow you to call one object's methods on a different object:

  (= foo (object x (fn (self) ...)))
  (= bar (object))

  (foo<-x bar)  ; self is now bar, rather than foo
In a sense, by having self as the first argument to methods, it's sorta-dynamic, rather than lexical. That's useful, but I dislike the verbosity. A way to get the same dynamicism while being less verbose would be great. I don't think ar's dynamic variables will quite work. I mean, yes, they'd work, but it'd still be too verbose for my liking.

---

P.S. If I understand correctly, ar does actually have extensible ssyntax... so I can try and add that in. That'll make it so much nicer to deal with objects. Here's a comparison. The first is the current way, and the second is how it would look with ssyntax:

  (get-attribute foo 'x)
  foo<-x

  (set-attribute foo 'x ...)
  (= foo<-x ...)

  (del-attribute foo 'x)
  (del foo<-x)

-----

1 point by Pauan 4963 days ago | link

Hm... I gave it some thought, and ar's dynamic variables might actually be okay. It'll make certain things a little bit clunkier, though.

For starters, there would be an implicit "self" variable. Rather than using "letr", "object" would use "w/self". The problem with doing that, is then you would need to call w/self yourself:

  (= foo (object ...))

  (w/self foo ...)
Why? Well, let's say foo grabs an attribute from bar:

  (= bar (object x (fn ...)))
  (= foo (object x bar<-x))
If you try to call foo<-x, the self parameterization will be all screwy, so you'll need to use w/self to fix that. One way to fix this would be to have a "bind" function or similar:

  (= foo (object x bind:bar<-x))
Clunky, but probably still better than giving every method a "self" attribute... I could even try making it so the object constructor automatically binds... that might work. In any case, it would be kinda hacky and clunky, but if it worked, we could get very concise code, while still gaining the benefits of pseudo-dynamic scope[1].

---

* [1]: I'm calling it pseudo-dynamic because I think it's mixing dynamic/lexical together. "self" should by default always be scoped to the object that is currently being used, but it should be possible to override that with "w/self".

Things get more complicated when an object's method calls a different object's method, or when you introduce things like inheritance... I can tell you how I would expect it to behave, the complicated part is actually making it behave that way.

Putting in a "self" parameter for all methods would basically solve this, but at the cost of verbosity, and a couple other things. For instance, you wouldn't be able to use existing functions like "is" or whatever as methods, because their argument list would be different.

So you'd have two types of functions: those designed for objects, and those that aren't. I think it'd be better to not have that kind of separation.

-----

1 point by rocketnia 4962 days ago | link

"Things get more complicated when an object's method calls a different object's method, or when you introduce things like inheritance... I can tell you how I would expect it to behave, the complicated part is actually making it behave that way."

This is where something like Lathe's secretargs might come in handy. If we made a secretarg for "the object the function is invoked on", then it's like every function in the language has it as a lexical parameter. Traditional function calls just pass a default value, and traditional functions just ignore it; you have to use special-purpose forms to define secretarg-aware functions and pass those arguments in. Is this close to what you have in mind?

A failure secretarg is how I hacked in failcall to Arc--and now JavaScript--and I'd also make a secretarg if I ever felt the need to implement keyword arguments. (Secretargs are essentially first-class keyword args without names, but one of them can hold a more traditional keyword table or alist.) The catch is that things like Arc's 'memo and JavaScript's Function#bind() aren't written with secretargs in mind, so they're not especially seamless with the language. But I think it would be cool to add secretargs to ar on a more core level. ^_^

Here's where I originally talked about secretargs: http://arclanguage.org/item?id=13825

And here are my implementations in Arc and JavaScript:

https://github.com/rocketnia/lathe/blob/master/arc/dyn.arc (search for "secretarg")

https://github.com/rocketnia/lathe/blob/master/js/lathe.js (search for "sarg")

-----

1 point by Pauan 4962 days ago | link

"If we made a secretarg for "the object the function is invoked on", then it's like every function in the language has it as a lexical parameter."

Hello dynamic variables!

---

"Is this close to what you have in mind?"

Sorta. The idea is close, but your way looks too verbose. I want it to be really short and simple, so having to define methods in a special way does not sound good to me. I want them to be plain-old functions.

I have an idea for what I'm going to do, and I think it'll work, because in ar, lexical variables always take precedence over dynamic variables, from what I can tell.

By the way, right now I only care about ar compatibility, since ar has nifty stuff like dynamic variables, which I've come to love. This not only should make the library shorter and easier to understand, but I honestly can't care much about compatibility, because my library kinda needs defcall, which isn't standardized[1] (and isn't even in Arc/3.1 at all)

---

* [1]: Or so I hear. I don't actually know.

-----

1 point by rocketnia 4962 days ago | link

"Hello dynamic variables!"

Secretargs happen to be implemented in terms of dynamic boxes. The benefit of secretargs is that you can always pass the default value just by making a normal function call; you don't risk propagating the existing dynamic value into the call. Whether that's important in this case is up to you. ^_^

Oh, and I don't think we can say anything's standardized in Arc at all. :-p The standards are just implicit assumptions around these parts.

-----

1 point by Pauan 4962 days ago | link

Note: when I said "standardized" I meant in the "de facto standard" way. Arc has plenty of those. :P

I agree that Arc has essentially no de jure standards[1], and I think that's a good thing.

---

* [1] The closest thing would probably be whatever pg does, but that would only concern the question of what official Arc is; other implementations would be free to either follow official Arc, or branch off into a different Arc-like dialect.

-----

1 point by akkartik 4963 days ago | link

I'm talking about inside the print method you mentioned. Say I want to print the length of the table inside print(), how would I get that?

-----

1 point by rocketnia 4962 days ago | link

I assume Pauan meant you'd change this...

  (object print (fn () "%custom printing!%"))
...to something like this:

  (letr self (object)
    (set-attribute self 'print
      (fn () (+ "#<table of length " len.self ">"))))
At least in the short term, 'til there's some kind of 'w/object to help out. ^_^

-----

1 point by Pauan 4962 days ago | link

No no no. I mean, yes, your way will work, but you can just use letr. No need to use set-attribute:

  (letr self (object print (fn () (+ "#<table of length " len.self ">")))
    self)
You can use set-attribute if you want to, but there's no need, since letr behaves like Scheme's letrec.

And soon I'll try changing it so you don't even need the letr:

  (object print (fn () (+ "#<table of length " len.self ">")))

-----

2 points by rocketnia 4962 days ago | link

Ohh, okay! Sorry, for some reason I thought 'letr was another name for 'ret or something. For some reason 'letrec didn't cross my mind. XD

In this kind of situation I use a utility I call 'named:

  (named foo bar)
  -->
  (ret foo nil (= foo bar))

-----

1 point by akkartik 4962 days ago | link

Ah!

What was that word I used to tactfully criticize your writing those many months ago? Well, Pauan is making you look good >;)

Another way to say that: you've taken the trouble to cater to my idiosyncratic dyslexias. Many thanks!

-----

2 points by akkartik 4964 days ago | link

Code! And no changes to compiler needed! I'm excited!

-----

1 point by Pauan 4963 days ago | link

Yeah, turns out it was pretty darn easy once I understood what the hell I was doing. First I tried grafting it onto Arubic/Python, which did work, but it was klunky and incomplete.

Then I tried writing an Arubic interpreter in Arc. Then I tried extending the ar compiler to turn it into Arubic... and then I finally realized that I could just... write it as a library in Arc, which only took me a few hours and works far better than my other attempts. :P

---

Moral of the story: Arc is good. Arc is very good. I guess I'm still too used to other languages, where if you wanted to do something like add in classes/inheritance/closures/lambdas/whatever, you'd basically need to create a new language, and write a new interpreter/compiler for it. But Arc is flexible enough that you can do a lot with it, and ar makes Arc a hecka lot more flexible than it already was.

(ar still needs something like defcall, though, because it's just so useful when creating new data types, but don't worry, I'll work on getting defcall into ar)

-----