That depends on whether you're using the default arc2.tar or if you're using Anarki (the git wiki with community changes---you should use it, it's better). The answer to the second question (define a new type) is yes either way:
(annotate my-type my-obj)
creates a new object with type my-type and representation my-obj. You can get the type with (type obj) and the representation with (rep obj). Types, however, have no associated behaviour.
The answer to the first question is no in arc2.tar (it's handled by the interpreter directly), but yes on Anarki:
defcall is like def, except instead of a function name you provide a type name. Then, whenever an object of that type is called in function position, its representation is passed in as the first argument (in that example, self), and the other arguments are passed to the rest of the parameter list. For instance, take a "shouter" type which wraps an output port and, when called, prints to that port in all uppercase:
The "Create your own collection" series should help a bit - these collections all use lib/settable-fn.arc, and with minimal modification should be useable with lib/settable-fn2.arc
'= is a macro, and therefore it needs to know how to assign to the variable at compile time, but the type information is known only at run time, but it should be possible to get something like:
(= (my-table tab 'test) "foo bar baz")
to work. It's a little more verbose and if you changed the name of the type from my-table to something else you would have to change every assignment.
speaking of gensyms, is there a reason uniq isn't defined in arc? one less axiom? (on a related note, I don't know why sig is defined in ac.scm, it doesn't seem to be otherwise referenced there)
Because if/when we have real gensyms, where if we have
arc> (= gs (uniq))
gs2003
then (is gs 'gs2003) returns nil, instead of t (which it currently does), we won't be able to define uniq in Arc. It's another datatype (the "uninterned symbol"), and thus it needs an axiom. The axiom could be something besides uniq (string->uninterned-symbol, for instance, or usym) if (isnt (usym "foo") (usym "foo")).
Aha. So uniq in its current form is really 'kinda-uniq. How important is it that gensyms aren't equal to each other? I mean, if "everyone knows" /gs[0-9]+/ is "reserved" for gensyms, then all we need do is not make symbols matching that pattern. Thus is the language minimaler.
I'm just being lazy. It can't be that difficult to implement ...
And does that really make the language more minimal? If we leave uniq as it is, we could move it to arc.arc, but we have the axiom that symbols of the form /gs\d+/ are forbidden; if we change uniq, uniq is an axiom, but we don't have to worry about formats.
Even if Arc were not based on mzscheme the change would be minimal: it takes exactly the same operations as creating a normal symbol, with the difference that it doesn't have to be interned.
"doesn't have to be", or must not be interned? I'm thinking the latter, if the point is to guarantee that no other symbol shall be equal to a gensym ...
Actually, arc just compiles everything down to Scheme; Scheme, of course, has full continuation support, and thus so does Arc.
That's a nice explanation of continuations, but one comment: I think (if I remember what I read correctly) that writing ccc in terms of mark-the-current-spot (or perhaps get/cc, or gcc :P) is actually impossible, but I could be wrong. The other advantage of ccc is that every flow control construct can be simulated using it (including, but not limited to, C's return).
> I think (if I remember what I read correctly) that writing ccc in terms of mark-the-current-spot (or perhaps get/cc, or gcc :P) is actually impossible, but I could be wrong.
that's the short-term conclusion i came to when i was fidding with it. i didn't exactly prove it was the case though so i was being safe
Hmmm, interesting. The "chaining" aspect reminds me of a pattern that people (e.g. almkglor: http://arclanguage.org/item?id=6955) have found with givens:
(givens x (+ 1 5)
y (* x 7)
(- y 88))
although of course here you have to explicitly name your variables.
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)))
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:
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
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 ...
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.
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?
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)))))))
(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)))
^^ 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]]
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>
I'm not sure what I think of the coding style, but there is a problem with your new macro. What would happen, for instance, if you had
(let this nil
(new push [push _ this] ; Anonymous function with an argument named _
pop (fn () (pop this))))
When we expand new, we get
(let this nil
(let this (table)
(= (this 'push) [push _ this])
(= (this 'pop) (fn () (pop this)))
this))
Now, your stack no longer works---it's modifying the wrong this! Moral of the story: never let names be bound in macros. The solution is to use a unique name, generated by the function uniq. The macro w/uniq lets you get as many uniqs as you need. To rewrite new using this, we would write
Notice how instead of this, you have ,g (which must be unquoted because g holds the name we want). And also notice how I've changed some names---that's because this is the obj macro from vanilla Arc. So moral of the story number two: use built-in Arc functions (the caveat, of course, is that the documentation is, shall we say, subpar, so good luck).
Your examples will all work, thus, if you change new to obj. As for the style... I agree that person-foo is ugly, but p!foo doesn't really feel right either. One solution is to switch to Anarki (http://arclanguage.org/item?id=4951) and use a combination of annotate, defcall, and nex3's defm macro (http://arclanguage.org/item?id=4644). Here's how this might work:
(def person (first last age)
; (annotate 'type object) creates an object of type 'type.
; The object's type is obtained by (type o), and the object's
; representation (the second argument) is obtained by (rep o).
(annotate 'person
(obj first first
last last
age age)))
; When an object of type 'person is used as a "function", e.g.
; p!first, this will be called (where self is p and field is 'first).
; Insert error checking if you want p!nonfield not to return nil.
(defcall person (self field)
self.field)
; See http://arclanguage.org/item?id=4644 for documentation of defm.
(defm full ((t self person))
(string ((rep self) 'first) " " ((rep self) 'last))
(defm until ((t self person) year)
(- year ((rep self) 'age))
; Examples
(= p (person "Eli" "Courtwright" 25))
(prn p!first)
(prn:full p)
(prn:until p 30)
This is a bit cumbersome, so you could define abstracting macros. Some of these already exist, for instance <plug>my tagged union system on Anarki (http://arclanguage.org/item?id=7364)</plug>, which would turn this code into
(vtype person (first string?)
(last string?)
(age (andf int? [>= _ 0])))
; And the same defms and examples from above.
Of course, this provides you with all sorts of nifty features; see the link for details. And I recall seeing other proposed object systems too, but I can't help you with those.
I actually used "obj" as a reference when writing "new", which is why they were so similar. I decided not to use a gensym because I figured that "this" would effectively be a reserved word with a common meaning and so that I could use "this" in my methods. However, your point about clashes is well taken.
As for "defm", it looks pretty nice. I guess I wouldn't mind calling "(until p 30)" instead of "(p!until 30)" so long as I don't have to call "person-until" just to prevent naming clashes.
I really dislike having to call "((rep self) 'first)" instead of "self!first", but as you point out, I might be able to define some macros to make this less verbose.
I guess for now I'll start playing around with Anarki. Hopefully the more knowledgable Lispers out there will settle on an object system that's general enough to be useful and concise enough to by enjoyable.
Ah, I see -- new is an auto-binding obj. That makes sense if you're including methods (which perhaps you shouldn't). It's just that usually when people do that it is a mistake :)
Actually, that was a mistake on my part; you can in fact write
(defm full ((t self person))
(string self!first " " self!last)
(defm until ((t self person) year)
(- year self!age)
because the defcall allows you to put objects of type person in that position. And now the only redundancy left is having to write (t self person) all the time, which you may or may not want to abstract.
The other missing feature is the inability to say (= p!age 42); for that, you need to override sref:
; Insert error checking so that you can't
; write (= p!species 'banana).
(defm sref ((t self person) value key)
(= ((rep self) key) value))
Again, <plug>the tagged unions do all of this for you (except for defining your own methods, which you have to do with defm and vcase/tcase)</plug>. Of course, they don't have inheritance.
That sounds excellent, and it makes me a lot more excited about using objects in Arc. I don't care that much about having to write (t self person) a lot, since it seems like an acceptably low amount of boilerplate.
However, out of curiosity, why do you have to write "t" instead of just saying "(self person)"? I read through the code for defm and argmap, but my Lisp reading skills aren't strong enough to decipher it.
Because parameter lists in Arc support destructuring. What that means is that anywhere you can write variable to bind a name to a value (such as in (let variable 10 (prn variable))), you can also write (v1 v2) to bind a list's first element to v1 and second element to v2. And (o v default) denotes optional parameters. Perhaps some examples would be clearer:
(with (i 1 v 5 x 10)
(let a (list i v) (prn "a is (1 5)."))
(let (b c) (list i v) (prn "b is 1 and c is 5."))
(let (d . e) (list i v x) (prn "d is 1 and e is (5 10)."))
(let (f (o g)) (list i) (prn "f is 1 and g is nil."))
(let (h (o j 42)) (list i) (prn "h is 1 and j is 42."))
(let (k (o m)) (list i v) (prn "k is 1 and m is 5."))
(let (n (o p 42)) (list i v) (prn "n is 1 and p is 5.")))
defm adds a (t var type) syntax; if you left out the t, you would have ordinary destructuring bind.
I feel like there ought to be a better way to do this--something that's less complex but handles more cases. In particular I'd like to be able to alternate OO and pattern-matching styles on the same datatypes. However, I haven't been able to come up with a solution that really satisfies me.
One other thing: wouldn't it be better to raise an error when an invariant is violated, instead of returning nil?
You can use OO structure-access semantics with tagged unions: (tu 'slot-name) and (= (tu 'slot-name) new-value). That's not the interesting part of OO, of course :)
I thought about returning an error, but Arc seems to be a little calmer about such things: (is (car nil) (cdr nil) nil), for instance. Of course, (car 'sym) errors. ::shrug:: I'm not sure about this, and it might change.
The purpose of a type system is to help find errors by failing early instead of letting bugs propagate through a system and show up in mysterious ways. Right now, you're actually making debugging harder because you're just wrapping bugs inside other bugs. Instead of figuring out why his binary tree turned into a cheeseburger, a programmer now has to determine (a) why his binary tree is nil (or why mutating it seems to have no effect), (b) which invariant he violated, and (c) what error caused the invariant to be violated.
Regarding (car nil) and (cdr nil), these return nil because it's sometimes convenient. For instance, here's a function that collects every other item in a list:
EDIT: Creation, reading (defcall), and writing (sref) all give errors if invariants are violated/nonexistent keys are accessed. Is there anything I missed?
Update: I (think I)[1] added rkts's suggestion of "solitary" tagged unions/variants, which act very similarly to plain structures except with the availability of a vcase macro (very much like tcase). They are defined with
(vtype name (arg1 pred1) arg2 ... (argN predN))
, where an argument without a predicate is implicitly (arg object) (you can't do this in tcase without extra parentheses, though). vcase works similarly to tcase:
(vcase var
name1 (expr1)
name2 (expr2)
...
nameN (exprN)
(optional-error-condition))
Note that unlike in tcase, the names aren't connected, but are merely defined by whichever vcase (or a tcase with a variant named the same thing as the union, which is what a vcase type is).
This required fixing a bug where you couldn't have a variant with the same name as its tagged union (because you then had (annotate NAME (annotate NAME ...)), and that is equivalent to (annotate NAME ...)). This means that you can do that now, but that *(~isa (rep tu) 'variant-type)&.
[1]: That is, I added something which I think is what rkts suggested; rkts will probably disabuse me of this notion rather quickly if I got something wrong :)
Yes, but. That only holds if in (annotate t1 (annotate t2 data)), we have (isnt t1 t2) (there was a thread about this somewhere, but I can't find it). Try it!
arc> (annotate 'foo (annotate 'bar obj))
#3(tagged foo #3(tagged bar #3(tagged mac #<procedure>)))
arc> (annotate 'foo obj)
#3(tagged foo #3(tagged mac #<procedure>))
arc> (annotate 'foo (annotate 'foo obj))
#3(tagged foo #3(tagged mac #<procedure>))
I was surprised too; that's why this was a bug I had to fix. I suppose this behaviour makes sense, but it does break the second of the two identities
(is (type (annotate t r)) t)
(is (rep (annotate t r)) r)
Huh. Have you tried it on PG's ArcN? It might have been me hacking this onto Anarki (I have multiple personalities. No, don't believe what I just said, that wasn't me. LOL). Don't have access to an arc right now, sorry ^_^
I remember that discussion; in fact, I had a response, http://arclanguage.org/item?id=3518, in which I proposed this (and I might have been working on this at the time).
The clearest advantage of distinct types is in things like binary trees:
you can't quickly tell what's a binary tree and what isn't in the same way, and you lose the invariant that a branch's left and right are both binary trees. At the same time, something more like vtype would remove
And you should be able to write a macro that will convert something nicer to that form, e.g.
(vcase var
variant1 (do-something var)
variant2 (do-something-else var)
(fail))
. I'll work on that and push it to Anarki.
I'm not convinced on the auto-binding; I implemented it that way because the resulting syntax was nice. It might make sense to have an option to work without auto-binding of names, but maybe it won't actually be an issue in practice. car and cdr are a bit atypical because (I hope) you won't be redefining a list type :)
For use cases: see Haskell code? :P I wrote this because I really liked them in other languages and in EOPL, and thought they might come in handy. If I come across somewhere in my Arc code where I use them (I haven't written too much new Arc recently) I'll report out.
I also wrote most of this a while ago (I think I started in March), and my coding has improved quite a bit since then, so the implementation could probably get cleaned up... :)
> If I come across somewhere in my Arc code where I use them (I haven't written too much new Arc recently) I'll report out.
Please do ^^. As an aside, it may be possible to transform the AST data structures in arc2c to tagged unions; we could transform the (if (alam ast) ... (alit ast) ...) to tcase forms. Care to try? It strikes me as a possible demonstration of your code, and may be useful to shake out bugs.
I likemaybe :) Also, it's simple and good for testing (e.g. it has a zero-ary type).
The arc2c thing sounds sensible---I have to fix a bug in tagged-union.arc first (you can't currently have a constructor with the same name as the datatype), but then I'll try to work on it.