I Googled and found http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.101...., which cites http://www.cs.indiana.edu/cgi-bin/techreports/TRNNN.cgi?trnu... as the source of the term. Apparently these machines are named after the things they're made up of. The K stands for "continuation code," the continuation, which is shaped like a stack, and the C stands for "control string," which is a lambda calculus expression with two extra kinds of terms for capturing and replacing the continuation (broadly speaking).
The original paper starts with CEK-machines, where the E stands for "environment," and shows them equivalent to a lambda calculus variant, first reducing them to CK-machines, CC-machines (where the continuation is shaped like an expression), and C-machines. Apparently this was a significant demonstration of how to formalize control operators like call/cc.
In terms of this "applicative syntax-rules" topic, probably the only part of CK-machines that matters is the notion that there's a stack being passed around. Even with that in mind though, I don't really understand it at all. ^^;
I have limited experience with this, but I'll take my best shot.
In Haskell, programs are built up out of pure functions (duh ^_^ ). A computation with side effects is represented as a data structure, a monad, which can be traversed just like any data structure. Executing a monad is just traversing it in a way that simulates the world model the monad is designed for.
Monad traversers come in various forms. The Haskell runtime itself traverses the IO monad. Oftentimes only a single traverser is used for any given kind of monad, like runState for the State monad. It's also useful to implement a new monad by translating it to an existing one and relying on that one's traversal to do most of the work (and in fact, this is pretty much necessary for any computation which needs to do things only the Haskell runtime's IO monad traverser can do).
The monad model supports many kinds of side effects, including nonlocal returns/exceptions (where the traverser stops traversing early) and nondeterminism (where the traverser has multiple choices for how to continue). A generator's "yield" operator is an example of an impure operation which not only suspends the calculation early, but also gives the calling code the option of whether or not to continue it. It can be implemented in a monad which combines those two techniques, and that's what they do here.
Thanks indeed! ^_^ I've gotta thank myself for my own comment, 'cause suddenly I think I understand how to take various side-effect-like concepts I've been interested in and actually turn them into Haskell monads. XD Choose-your-own-adventure monads, FSM monads, and static single-assignment monads, here I come, maybe....
I think waterhouse was saying that built-in functions would auto-unwrap their arguments by default, but it would just be a default, and you could override it with custom behaviors if you wanted to.
"If desired, you could add polymorphism--redefine + to dispatch on numbers tagged as numbers mod 7 or something, which it would discover by calling "type" on it before stripping off the tag. However, the strong default would be to treat it like the thing that it is --when "car" is called on the "fn"-tagged list, it's probably being called by "ar-apply", which knows exactly what it's doing and would like "car" to shut up and give me the car, thank you."
I guess in a way, this is another suggestion to add a default behavior for something that is currently an error, like letting the function call (1 2 3) return the list (1 2 3). But then waterhouse probably doesn't have that motive in mind, considering the opinions expressed at http://arclanguage.org/item?id=12841 and http://arclanguage.org/item?id=13827, so it's probably just a side effect, for better or worse. ^_^
Yes, except I don't think this solves my criticism. It still seems like a good idea to get rid of rep, though. They're orthogonal concepts, and both ideas can be used at once.
Then again, if you're going the route of attaching arbitrary tags to objects... might as well go the full message passing approach, which is what Arubic is doing.
"In Racket, you don't exactly have access to the low bits of a cons cell for tagging purposes. Instead we have to make a compound data structure--tag things with a vector--and we need a "rep" function to extract the object itself. But this is an implementation detail, and it can and should be hidden."
Isn't it the other way around? Why are "low bits" not an implementation detail?
---
"If you pass "car" a tagged cons, or you pass "+" a tagged number, it just strips off the tag, finds that this object is indeed something it knows how to add, and just adds it."
If I define a new type, it makes little sense for it to inherit the qualities of the particular old type I'm using to implement it. That idea does nothing but expose implementation details.
---
"The arbitrarily nested tagging is technically possible here, but I think people haven't run with it very much because the interface sucks."
The interface indeed sucks a bit, 'cause [annotate 'foo _] doesn't wrap something that's already a 'foo. That means if you want to make a new type which wraps a single arbitrary value, then you need to do something like [annotate 'foo list._] so that you don't get mysterious bugs wrapping things of type 'foo. I end up having to do this all the time.
A long time ago (http://arclanguage.org/item?id=12076) I didn't like 'annotate and 'rep because I found them wordy, but I got over it. :-p Ssyntax goes a long way; once I started accessing compound types using things like (let (a b c) rep.foo ...), rep.foo.1, and rep.foo!fieldname, the "rep." part didn't seem so bad. I said something similar recently at http://arclanguage.org/item?id=14373, but I don't expect you to read that thread. ^^;
---
Speaking of that thread, though, this part might be relevant:
One could go all the way down, making a (deftype wrap-foo unwrap-foo isa-foo) form that eliminates the need for 'annotate, 'rep, 'isa, and 'type altogether. In this example, 'isa-foo would be a function that indicates support for 'unwrap-foo, and 'wrap-foo would be a simple data constructor that makes an encapsulated value with nothing but innate support for 'unwrap-foo. (http://arclanguage.org/item?id=14265)
I consider that to be a great way to get rid of implementation details, since someone can extend 'unwrap-foo and 'isa-foo for a new type if they want to. However, I'd also like to leave in 'annotate and friends, just so people who'd really like to dig into the details can do so.
---
"Hmm, one possible flaw occurs to me. If something is (tag a (tag b x)), a user-defined function won't be able to extract the "b" using 'rep. However, I don't think that's much of a problem: if the thing that tagged the (tag b x) as an "a" wanted to allow its callers to do inheritance or whatever on the "b" tag, it could instead have returned (tag (list a b) x)."
As I've mentioned recently (http://arclanguage.org/item?id=14176), I only expect 'type to determine the concrete representation of something, since it naturally arises (in my experience) that a value has one and only one concrete representation.
With the way I code, saying (tag a (tag b x)) means I'm making an "a"-typed value that has a "b"-typed value that has "x". I don't consider them all to be the same value, so the flaw you're talking about is relevant to me, and your example workaround solves a different problem.
> Isn't it the other way around? Why are "low bits" not an implementation detail?
The implementation detail is that "you can't tag an object without wrapping it in a compound data structure that old functions will have to be told to reach inside to get the original object".
"Hiding implementation details" isn't something to be done for its own sake. You want to hide implementation details when they're uglier than the semantics you want to convey; e.g. it's impossible to use a MUL instruction to check if the result of a multiplication is a bignum and allocate one and return a tagged pointer to it if necessary, but keeping track of all that is a pain; so instead we have a generic * function that hides all this scrambling around, and we can think of all integers as a single type of object. Exposing that stuff to a user who isn't extremely concerned with performance just sucks. It's bad because the interface sucks, and we'd identify it as "exposing implementation details" because that was the reason it happened.
On the other hand, does anyone want to hide the fact that lists are implemented as conses and that you can take their car and cdr? Not me. That stuff is useful.
> If I define a new type, it makes little sense for it to inherit the qualities of the particular old type I'm using to implement it. That idea does nothing but expose implementation details.
When you first define a new type, no preexisting functions will know how to handle the new type. You have two choices for what should happen in the absence of such instructions. Either they can give errors and fail (unless you use "rep", in which case they do their job as usual), the way they do now, or they can do their job as usual, the way it would be under my proposal. I think the latter choice is clearly--I would say strictly, unless you find the error messages useful--superior.
Now, if you define a new type, the first thing you'll probably do is define some functions that handle the new type, and they will likely need to use old functions that manipulate the objects that your new type is made of. (Perhaps the functions you're defining will be intelligent, wrapped versions of the old ones.) As mentioned, either they have to use "rep", or they don't.
And if you want to define a second new type that's supposed to be a subtype of the first one, then you can still build that on top of the provided "tag" and "type". (I think this answers your last point.) For example:
;Single inheritance. The path of inheritances is an improper list.
;Builtins will have atomic types; you could give something an
;atomic type to pretend it's a builtin.
(def inherit-tag (typ x)
(tag (cons typ (type x)) x))
(def inherit-type (x)
(if (acons (type x))
(car (type x))
(type x)))
(def inherit-rep (x) ;more like "cast to supertype"
(if (acons (type x))
(tag (cdr (type x)) x)
x))
(def is-a (typ x)
(if (atom (type x))
(is typ (type x))
(or (is typ (car (type x)))
(is-a typ (inherit-rep x)))))
If you wanted "has-a" type semantics or something, you could figure out a similar way to define those. (Also, it occurs to me that you could use "tag" to identify your tagged objects, as in (tag 'inheritance-tagged (list (list 'type1 'type2 ...) 'implementation-type x)) or something.) I think you can build things on the type system just as much as before, and meanwhile the initial implementation is cleaner.
[Hmm. Come to think of it, given that the user doesn't have access to the internal "rep", and the built-in functions don't use it, it's meaningless (and useless) for (tag 'a (tag 'b x)) to return nested vectors; I'll make it return the same thing as (tag 'a x). So in the language primitives, everything has a single type--which would likely be just a symbol, but conceivably an Arc standard library might have things that do inheritance with lists of symbols. Anyway, this doesn't change anything I've said above.]
"The implementation detail is that "you can't tag an object without wrapping it in a compound data structure that old functions will have to be told to reach inside to get the original object"."
When I use 'annotate, what I want is to do this: Take a value that's sufficient for the implementation of a new type, and produce a different value such that I can extract the old one using an operation (in this case 'rep) that I can't confuse for part of the type's stable API.
Any "tag" semantics that overwrites the original type is useless to me for that.
I do think "wrap" is a better name for what I want than "tag" or "annotate," and that's the word I reach for when naming a related utility or recreating 'annotate in a new language. I don't mind if you say that tagging is something different. ^_^
---
"You want to hide implementation details when they're uglier than the semantics you want to convey"
From a minimalistic point of view, any accidental feature is ugly complexity, right? But I grant that having easy access to accidental features is useful for a programmer who's writing experimental code.
---
"On the other hand, does anyone want to hide the fact that lists are implemented as conses and that you can take their car and cdr? Not me. That stuff is useful."
I consider Arc lists to be conses by design, not just by implementation. As you say, conses are a positive feature.
On the other hand, the core list utilities would be easier for custom types to support if they relied on a more generic sequence interface, rather than expecting a type of 'cons. This isn't hiding the implementation so much as deciding to rely on it as little as possible, but it's somewhat causally related: If I don't rely on the implementation, I don't care whether or not it's hidden. If the implementation kinda hides itself (the way the body of a 'fn is hidden in official Arc), then I tend to leave it that way, and it can come in handy as a sanity check.
---
"And if you want to define a second new type that's supposed to be a subtype of the first one, then you can still build that on top of the provided "tag" and "type". (I think this answers your last point.)"
No... I said this:
With the way I code, saying (tag a (tag b x)) means I'm making an "a"-typed value that has a "b"-typed value that has "x". I don't consider them all to be the same value[...]
There's no subtype relationship going on here. For comparison's sake, if I say (list (list 4)), I'm making a cons-typed value that has a cons-typed value that has 4. At no point should my conses inherit from 4, and at no point should my "a" inherit from "x". That's just not what I'm trying to do.
---
In any case, just because I like the status quo (or my own spin on it) doesn't mean I shouldn't give your approach a chance too.
So, as long as (tag 'foo (tag 'bar "hello")) makes something nobody can tell is a 'bar, it's consistent for them not to know it's a 'string either. But then what will core utilities which can handle strings and tables (like 'each) do when they get that value? Do they just pick 'string or 'table and assume that type by default? If they pick 'string, won't that make it seem strangely inconsistent when someone comes along and expects (each (k v) (tag 'baz (table)) ...) to work?
Seems like there's a significant disconnect in this thread.
I think d0m's saying: Why do Arc folks do things like X by making a new language? Shouldn't we put features in Arc itself, if it's going to be a language people will actually use?
I think aw's saying: Why not ask instead, "How can I do X in Arc?" In fact, here's a way I do X without making a new language.
I think Pauan's saying: I'd like to do X, and aw's approach seems clunky. I think a new language would be better.
---
As I see things, d0m, we pursue our own languages because there are certain improvements we want to make that take a complete rework of the language.
Python interop is a great example. Clojure-Java interop is possible because they both run on the same rather high-level platform, the JVM. Official Arc runs on the Racket runtime, and Python (at least CPython) is its own whole runtime, so there aren't many ways to bridge the gap except for the C FFIs and the kind of IPC aw's lang hack uses. One of the best ways to get a clean integration between Python and Arc would be to reimplement Arc and/or Python to run on the same high-level platform--and yet that means reimplementing a language, rather than improving an existing one.
Or does it? The reimplementations may already exist. If you could use Jython for your Python project, then you could probably achieve a good degree of interoperability with Jarc and Rainbow, which are high-quality Arc implementations for the JVM.
---
But really I think Python interop was only one example, and your point was any that features we could put into the main Arc language should be put there. In that case, you'll need to define what counts as Arc.
There's an official release of Arc, maintained by Paul Graham, but keep in mind that he only updates once in a blue moon and tends to leave out features that aren't obviously necessary to him. A brief language implementation is one of his primary goals for Arc.
We do have Anarki, a community fork of Arc, but keep in mind that Anarki is held back a little by a desire to be similar to official Arc, so that the next time Paul Graham releases an update, it's easy to merge it in. This similarity also makes Anarki a bit more useful in practice, since it's compatible with most official-Arc code people write.
We could probably relax that policy on Anarki, but what big changes should we allow? I'd make radical changes and turn it into Penknife. Pauan would make sorta big changes and turn it into Arubic. It could be useful to make radical implementation changes and turn it into aw's ar, and akkartik may want to radically reorganize the project's file tree.
All in all, Arc's philosophy isn't well-defined and persuasive enough to drive us all toward the same future. Although we do want to make big changes, it's easy for us to be unsure about them or disagree. So we tend to experiment with big ideas by implementing them in our own language projects, where at least we know what we like.
If it helps, think of it this way: We don't make our own languages just to let Arc stagnate. We make our own languages so that we can improve Arc in various ways all at once. This fracturing is kind of unfortunate, but we still do build upon each other's ideas, so we're just advancing the state of the Arc in an unusual way. ^_^
Still, I suppose it means we have a community that's a little hard for non-language-implementors to appreciate. There are some less vocal people who talk about Arc only for Arc, like thaddeus and kens, but even then the topic can diverge to how our fractured Arc variants deal with the issue. :-p But you know, I wouldn't have it any other way. Maybe if the community were larger, we could have separate forums for in-Arc and around-Arc discussions.
I suspect that for the most common uses the disadvantages of providing language interoperability in the same process outweigh the advantages, and so personally I'd be surprised if implementing Python and Arc in the same runtime would have much impact on the uptake of Arc. I could be wrong of course... and then I'd be surprised :-)
With respect to there being lots of variants of Arc, I think about Arc as a personal programming language. Thus that it allows extensive personalization, and people publish many personal variants, is to my mind a feature not a bug.
This diversity of course raises its own issues: we're concerned not just with interoperability between Arc and other languages, but interoperability between variants of Arc! :-) But solving this provides a more powerful platform than one stuck with providing the lowest common denominator of standard language features to ensure interoperability.
I also think that the idea of personal programming languages remains largely unexplored territory. Thus if there is significant uptake of Arc, I imagine it may not come at the expense of other languages, but instead it could be for new uses. Because of this belief I tend not to pay much attention when someone says "Arc should have X because other languages have it", but I'm very interested when someone says "I'd like to use Arc myself, and I'd be able to if only it had X".
"I suspect that for the most common uses the disadvantages of providing language interoperability in the same process outweigh the advantages"
I've never thought of that. I'm used to using Racket stuff from Arc without hesitation, but then the stuff I use is provided with the Racket language, like parameters and regexes. Most of my non-Arc personal code is in Java or Groovy, and I'm very glad I can use it from Rainbow, but I haven't actually done that yet....
Still, I can personally attest that this kind of feature has played a role in my adoption of languages, regardless of whether I've used it. :)
Through-out this whole thread I've wanted to kick in and say: "boy I wish Arc was built on top the jvm, similar to Clojure's implementation", but then I also realize one might as well just switch to Clojure if that's what one wants.
Would have been be nice though. Smart move by Rich.
The way clojure implemented it allows for seamless integration between libraries. With clojure I can load and easily make use of any java code out there. I can use cron job libraries, database libraries etc. etc. And I really don't require much java knowledge, just a basic API used to call the library/jar file. This gets people on-board, hence larger community, building all kinds of kewl apps/libraries... More solutions, more knowledge base... Just more options.
With arc, the language syntax and base libraries are awesome, but you scratch your head more often, wondering" well how the hell am I supposed to do that. " And these problems/features are not small roadblocks... For example, if I need to use Arc connecting to oracle? - good luck, it's not going to happen.
If arc were built atop the jvm, the way clojure is... These problems all go away.
Thanks for the offer. I think real database connections to arc could only improve the adoption + user happiness levels, but I will suggest connecting to oracle was just an example for the point I was trying to make.
What I am suggesting is not new or even surprising. Arc has no depth for libraries. Rich solved this problem right from the get-go by having tight integration with java. Arc would have had similar success had it done the same thing - too bad really. And dropping into Scheme just doesn't cut it.
I'm suggesting users of arc will google things like 'arc oracle', or even 'scheme oracle' which returns jack. With Arc you get Nada for google answers + you get unanswered posts such as these.
step-by-step recipes to connect to a myriad of databases. This was made possible through Clojures tight integration with java. Clojure can import java libraries with 1 simple import statement.
Let's not forget that Clojure is a newer language than Arc - correct?
Now, I'm just looking at the title of the thread "Effort to make it easy to use in production?", and thinking - yeah, I can understand, that's why I moved over to Clojure and its too bad Arc didn't do this.
Sure I'm no expert in the programming language industry, but if I were to come up with a new spiffy language I'd rather do what Rich did. Arc could have been built atop the jvm - correct? It was just easier not to, considering arc was not about being practical, it was about attaining some elusive mystical idealized syntax that will be practical in, possibly, a hundred years from now.
Sorry if this sounds like a rant, but it just seems like the original poster wasn't getting much love, and I can appreciate what and why he/she was asking.
When I started work on my Arc runtime project, the first thing I looked at was what I thought would be the best platform to write the runtime on top of. Racket seemed to be moving away from Arc in some ways, so I didn't start with the assumption that Racket was going to be the right choice. I looked at a number of options, and concluded that as far as I could tell that Racket still had the best mix of features.
The JVM naturally has the advantage that it's easy to call Java libraries, but it has the disadvantage that it has no built-in support for tail call optimization or continuations. Since it's not particularly difficult to make use of libraries running in a different process, it would be easy enough for me to use Java libraries from an Arc implemented in Racket if I wanted to, and so I had no compelling reason to write ar on top of the JVM.
There is a long history of languages written for people who want to use the JVM but who also want to be able to use a less terrible language than Java: Jython, Groovy, Clojure, etc. Since Clojure is a particularly nice language, it didn't surprise me that it rapidly gained popularity among people who want to use the JVM.
For any particular project that I may be working on, there are a mix of features that I need for that project. If I were working on a project that primarily needed to call Java libraries and I didn't need the features unique to Arc, I would most likely use Clojure myself (or Rainbow or Jarc, if it turned out that one of those were suitable to my project). If I were working on a project where the features of Arc were important and I also needed to call some Java libraries, I'd likely write it in Arc calling out to Java libraries.
Since this is a forum for Arc, I answer questions primarily from the perspective of Arc. Since typically a hundred people will browse a forum for each person posting, I primarily answer questions for the majority reading. For example, if someone makes a statement that is wrong, I will say that they are wrong, so that someone else reading the forum isn't left with incorrect facts.
When someone complains about Arc, I tend to suspect that either there's something they want, or else they just feel like complaining. People wanting things is useful. If someone actually wants X and says so, and we come up with a solution for them, then we also help the hundred other people who want that thing as well. Other the other hand, and I'm not saying that anyone has actually done this, but if someone were to sit at a computer and type in a complaint that other people haven't done the work to make Arc a popular language faster than some other language has become popular, that doesn't strike me as being particularly useful.
I'm not against complaining. Complaining is often a useful first step towards finding a solution. However, while I'm not trying to pass judgement, I am trying to discern whether someone is complaining because they just want to complain or because they want a solution.
Again, when I answer a question, I answer a question primarily for people in general reading the forum; and from the perspective of people wanting to use Arc. Thus if Clojure is a better solution for you for most of the projects that you work on, that's fine, I agree you should use Clojure. However when I read "for example, if I need to use Arc connecting to oracle? - good luck, it's not going to happen" my immediate concern is that someone else is going to read that and might be left with the impression that it's prohibitory difficult to connect to oracle from Arc... which, while it's certainly harder to connect to oracle from Arc than it is from Clojure, isn't all that difficult.
Being difficult or easy is a relative thing. The hours of investment by grade-a developers contributing to making these production grade feature-x's in other languages leads me to feel pretty comfortable suggest that having them,
or a dozen other similar feature-x's would not be all that easy.
Sure, if you have spent hundreds of hours doing low level hacking between racket<->arc you can suggest these things are not all that hard, but the reality is that many of these feature-x's are just not going to happen.
Chances are that mainstream features are not a priority for the members here... And that's my message to the OP: go use Clojure or something having the features you want. I say this from 2 years of following arc, the arc forum and its members. There are probably a dozen submissions/comments about db connects and FFI, yet to this day nothing works.
The people on this forum, who know me, know I like the arc language, often promote it and that I'm not here to tear it apart, but I'm going to be real about it and make sure the op gets the right message (from my perspective).
> "If you like the Arc, then instead of complaining that other people aren't figuring out what you'd want to use for you, why not take the time to help and describe what you would need in a db connection library and FFI?"
I'm thinking if you re-read the thread starting from my first comment (http://arclanguage.org/item?id=14539), you'll hopefully come to understand that I haven't been complaining, but rather I have been defending/substantiating my suggestions and thoughts.
As for why I don't take time to describe some spec, well because I'm not asking for anything and I've chosen to spend the bulk of my time learning Clojure and building applications. I guess you could say I take my own advice.
> "If you want some love on the forum, there's an easy way to get that:make a useful contribution."
It's pretty obvious, based upon your replies, that you don't see my posts as valuable contributions, but maybe you'll come to understand these posts are intended to help the OP rationalize his topic, not to gain some form popularity/acceptance with or by you.
If you like the Arc, then instead of complaining that other people aren't figuring out what you'd want to use for you, why not take the time to help and describe what you would need in a db connection library and FFI?
Or, you could add a page to the wiki describing the ways that Clojure is better than Arc.
If you want some love on the forum, there's an easy way to get that: make a useful contribution.
If the JARs are in the classpath, the classes are just as accessible as any standard platform class like java.util.Map. I give a demonstration here, where I explain the custom interface I built so I could sometimes use the same code on both platforms: http://rocketnia.wordpress.com/2010/06/01/lathes-arc-to-impl...
Since then, Jarc's actually added Rainbow compatibility functions, so you might be able to get away with writing code just for Rainbow without leaving Jarc too far behind. Rainbow's faster, but Jarc has a more straightforward implementation.
I recommend having one of the JARs be a language with eval, like Groovy, JRuby, or Clojure. Jarc and Rainbow convert certain values to/from Arc in ways that are nice most of the time but sometimes lose precision, like the distinction between Boolean.FALSE and null.
Bravo. I'm bookmarking this comment. Next time we get a question about fragmentation I'll point to the final section.
---
I didn't plan to hack on programming languages when I started playing with arc. Creating new languages/implementations is an occupational hazard of hacking on arc. Or maybe it's a pleasant side effect that we get for free (a viral - and rapidly mutating - language). Depends on how you look at it.
---
I think it's beneficial that these variants aren't splitting off into their own communities. They're able to recombine features from each other. wart stole its iteratively-refined interpreter cond from ar. Pauan's gotten me thinking about message passing.
I agree with your approach here, and it's what I've been planning for Penknife too. It opens the door another issue though: If you end up making two instances of a language core or a library, what if you want a value from one to mean the same thing in the other, but that value's meaning depends on something each module generates for itself, like a gensym or a mutable data structure?
My vague plan for Penknife is to avoid having modules prepare their own dependencies (in the spirit of http://gbracha.blogspot.com/2009/06/ban-on-imports.html), and also to introduce "spoof" bindings in namespaces so that modules which would generate new globals would harmlessly do nothing at that point, instead continuing on using the value that was already there. It's kind of a kicking-and-screaming form of Common Lisp's 'defvar (which uses an existing definition if it exists)... and you know what, 'defvar is probably a better overall plan. XD
Anyway, my solutions may not be the best ones for you. ^_^ Just want to get you thinking about it.
I think the question of configuration and dependencies is a very interesting one. I've also found that it can be too inflexible to have libraries declare their own dependencies (through the usual "import" or "require" statement at the top of the library). My new version of the hackinator (https://github.com/awwx/hack) isn't doing enough to be useful yet, but it's an attempt to describe dependencies between libraries in a declarative way outside of the libraries themselves.
It opens the door another issue though: If you end up making two instances of a language core or a library, what if you want a value from one to mean the same thing in the other, but that value's meaning depends on something each module generates for itself, like a gensym or a mutable data structure?
Without a more specific example I can only guess... that if you have two instances that both depend on the same thing, you can either have one instance import that thing from the other; or else factor out that thing into a third location that the two instances independently import.
I like your approach a lot, aw, but I too noticed this issue. I think it can be overcome just by changing the idiom slightly.
First, some definitions:
(def fn-latemac (mac-getter)
(annotate 'mac
(fn args `(,(mac-getter) ,@args))))
; Not to be hypocritical, let's implement 'latemac as if by using
; 'latemac.
(mac latemac (globalvar)
`(,(fn-latemac:fn () fn-latemac) (fn () ,globalvar)))
Now we can define your example 'foo and 'bar like this:
(mac foo ()
`(,latemac.do
(,latemac.prn "this is macro foo expanding into bar")
(,latemac.bar)))
; Oops, now let's define 'bar. Go go, late binding!
(mac bar ()
`(,latemac.prn "yo, this is macro bar"))
As you can see below, this already works in the current version of ar. ^_^
arc> (foo)
this is macro foo expanding into bar
yo, this is macro bar
"yo, this is macro bar"
arc>
(def bar ()
(prn "hi, this is bar"))
*** redefining bar
#<procedure:g1590>
arc> (foo)
this is macro foo expanding into bar
hi, this is bar
"hi, this is bar"
Essentially, these are hygienic macros at this point. They use the syntactic closure technique for hygiene, in this case implemented using regular old lambda closures. ^_^ A syntactic closure approach has undesirable properties like making the expansion hard to code-walk or serialize, but those activities seem rare enough anyway, probably because they're already difficult. I think this'll do just fine as far as modules go.
I bet Common Lisp's own dynamic scope support would work just as well, and it would probably be friendlier with conditions and restarts. Here's how it might look:
"yield still doesn't lazily generate sequences. In the absence of first-class continuations that would require code-walker-based support for coroutines."
I sat on the old ugly approach for a few days before realizing it was keeping me from moving forward. How could I forget defvar?? Anyways, I should have just thrown the crappy code over the wall sooner and gotten your feedback.
---
I briefly considered threads for coroutines[1] but recoiled. I may warm to the idea after agonizing over other options ^_^
I had no clue whether your replacement of 'let would work with special variables. If it does, awesome. ^_^
By the way, I used [] for the initial value of * yield* just because I didn't know what you wanted to do there. Actually, it would be amusing for 'yield to drop to the debugger by default and coroutine with the user.
---
"I briefly considered threads for coroutines but recoiled. I may warm to the idea after agonizing over other options ^_^"
I just wanted to make sure the idea was out there in the open. I don't actually have an opinion for or against it. ^^;
yes, right now in Arc 3.1 if I catch (err "foo") with on-err, I have to use details to extract the string "foo", but with your idea of using raise we'd get the string "foo".
So in this way your idea and my idea would work out the same: both would pass the string "foo" to the handler when (err "foo") was used.
A difference would be primitive errors generated by Racket, with my approach the handler would get the string like "/: division by zero" while with your implementation the handler would still get the exception object.
I don't think we'd want it to matter if errors are generated internally by Arc or Racket, so assuming that we do want to continue to raise errors in Arc with (err "foo") then I'm guessing we'd want to special case Racket exceptions and pass the exn-message string to the handler so that it would end up working the same way.
By the way I don't think you do want to use call-with-exception-handler; if the handler returns then then Racket invokes the next exception handler in the chain, so it's like re-raising the exception. But the with-handlers form works fine with arbitrary values raised by raise.
"I don't think we'd want it to matter if errors are generated internally by Arc or Racket, so assuming that we do want to continue to raise errors in Arc with (err "foo") then I'm guessing we'd want to special case Racket exceptions and pass the exn-message string to the handler so that it would end up working the same way."
That actually makes me wonder if Arc should have access to 'raise as a separate function from 'err. After all, if we catch a Racket exception as a string, we lose information.
For all I know, losing that information isn't so bad. I almost always design code so it never throws exceptions that should be caught, so I don't know what information is useful to have in an exception. I've only encountered two places it makes sense to capture an exception:
- When the exception implements an escape continuation, in which case the necessary information is the unique identity of the catch point.
- When the program is something like a REPL or server that handles multiple commands and should typically recover from an error encountered in any one command. In this case, the necessary information is whether the error should be considered severe enough to take down or restart the particular command loop in progress. The usual approach to representing this information is a hierarchy of types such that, if you say exn:fail is okay, you implicitly say exn:fail:contract:divide-by-zero is okay. Is there a better approach?
---
By the way I don't think you do want to use call-with-exception-handler; if the handler returns[...]
"...the exn:break exception typically should not be caught..."
"Before any predicate or handler procedure is invoked, the continuation of the entire with-handlers expression is restored, but also parameterize-breaked to disable breaks."
Would it be better or worse to switch to 'with-handlers*, which doesn't disable breaks?
"Okay, I just found exn-message... this really is a lot clunkier than it could be. sigh I feel kinda silly now, but I'm gonna leave this post here, in case somebody else has the same problem. I had already looked at the Exceptions documentation, but somehow missed that function. To my credit, the documentation for exn-message is pretty much... nonexistent. I accidentally learned about it while reading ac.scm."
In the documentation for the 'exn struct (http://docs.racket-lang.org/reference/exns.html?q=exn#(def._...), they show that the fields are "message" and "continuation-marks". They don't document 'exn-message and 'exn-continuation-marks because those are just generic struct getters. It's as though 'struct is a macro and 'exn-message and 'exn-continuation-marks are generated automatically, but I think it's a bit more integrated into the module system than that.
Sure, it has parentheses, uses the keyword lambda, provides lexical scope, and emphasizes macros — but don't be fooled. PLT Scheme is no minimalist embodiment of 1930s math or 1970s technology. PLT Scheme is a cover for a gang of academic hackers who want to fuse cutting-edge programming-language research with everyday programming. They draw you in with the promise of a simple and polite little Scheme, but soon you'll find yourself using modules, contracts, keyword arguments, classes, static types, and even curly braces.
Incidentally, the very same concern for comprehensiveness, practicality, and compatibility that (I believe) has damaged the purity of Racket's design makes it a nice platform for implementing Arc. That's no mistake either; Racket is designed to support lots of sublanguages besides Scheme, like Typed Racket, the stateless servlet language, etc.
Yes... I picked up on that... after stumbling upon exn-message in ac.scm. I did try skimming the struct documentation as well, but it doesn't seem to tell you how to access the fields.
From what I can tell with exn-message, if you create a struct "foo", with a field "bar", it will automatically create a function "foo-bar" to access it?
That's really unintuitive/clunky in my opinion. Especially compared to say... Arc, where it would just be (foo 'bar).
---
"That may be true, but Racket is much more complicated than Scheme has to be, and that's not a mistake."
Yes, but Racket feels like it's more complicated than it needs to be. Then again, perhaps there's somebody out there that needs this fancy-pants exception stuff. :P
And even though Scheme may be more minimal, it still has a few parts that seem bloated, like (let ((a 1) (b 2))) which is pretty unfortunate since I would expect `let` blocks to be pretty common! Thus, I see Arc as being more minimal than Scheme, in some regards.
"From what I can see with exn-message, if you create a struct "foo", with a field "bar", it will automatically create a function "foo-bar" to access it?"
I think that's what it says, though it isn't especially clear about it.
"Especially compared to say... Arc, where it would just be (foo 'bar)."
Racket structs are not the same as hash tables. For instance, they can have inheritance, and they can override 'equal? using 'prop:equal+hash (http://docs.racket-lang.org/reference/structures.html). They're not especially orthogonal features, but structs are specifically for custom data types, while hash tables are for associations.
"And even though Scheme may have been designed for minimalism, it still has a few parts that seem bloated, like (let ((a 1) (b 2))) which is pretty unfortunate since I would expect `let` blocks to be pretty common!"
Actually, I think that kind of let block is more consistent with the goals of Racket. It makes it easier for Racket to give detailed error messages when part of the let is missing.
Of course, the Arc approach is to make the syntax get out of the way so we can identify the error before it gets to the compiler. :-p
"I linked you to the reference, which is targeted at people who already generally know what's going on but need specific information. Here's a guide entry: http://docs.racket-lang.org/guide/define-struct.html "
Oh neat. I guess I was expecting documentation somewhat more like Python, where they're more likely to tell you that kinda stuff in the docs, rather than requiring you to go read the tutorial. :P That could come in handy, thanks.
---
"Racket structs are not the same as hash tables. For instance, they can have inheritance [...]"
Cough. Prototypes. Cough.
---
"[...] They're not especially orthogonal features, but structs are specifically for custom data types, while hash tables are for associations."
Except in Arubic you do use tables to make custom data types. :P Besides, that's not really the point. I'm not saying structs need to be implemented as hash tables, just that you could call them like as if they were hash tables. So rather than using (exn-message foo) you'd use (foo 'message).
Arc overloads function calling on tables, lists, strings, etc. So why couldn't they have done that with Racket? I guess one reason would be for type checking... Racket seems to be the kinda language where they prefer strong types. In which case, okay, I can't really argue against that, if that's Racket's goal. Still feels clunky to me.
Interestingly enough... the way things are going with Arubic, it looks like I'm essentially copying Lua. Lua puts a strong emphasis on tables, similar to how Lisps put a strong emphasis on lists. It also has prototypical inheritance. It also uses foo.x as syntax sugar for foo["x"], which is similar to Arc:
(foo 'x)
foo.'x
JavaScript also shares those properties with Lua, so I guess you could also say I'm copying JavaScript... but only the parts I like, which are primarily objects and prototypical inheritance (Arc already has lambdas and closures).
"Actually, I think that kind of let block is more consistent with the goals of Racket. It makes it easier for Racket to give detailed error messages when part of the let is missing."
How so? How does (let ((a 1) (b 2))) give more information than (let (a 1 b 2))?
Besides, I'm talking about the `let` in Scheme, so the whole "it makes sense in Racket!" thing doesn't really apply. :P How does it make sense in Scheme?
"I guess I was expecting documentation somewhat more like Python, where they're more likely to tell you that kinda stuff in the docs, rather than requiring you to go read the tutorial. :P"
It's not an organizational thing. Here's where they explain it the reference:
A struct form with n fields defines up to 4+2n names:
- [...]
- id-field-id, for each field; an accessor procedure that takes an instance of
the structure type and extracts the value for the corresponding field.
The reference even links to the guide, for what it's worth.
Depending on the context, the idea of a hash table with a prototype may be a contradiction in terms. For instance, if by "hash table" we refer to exactly the hash table implementation Racket has today, then it simply has no prototypes. :-p
There are contexts where it does make sense, particularly when talking about hypothetical languages, but if a single language tried to make sense of every idea people had for it, it'd be pretty darn bloated. ^_^
---
"I'm not saying structs need to be implemented as hash tables, just that you could call them like as if they were hash tables. So rather than using (exn-message foo) you'd use (foo 'message).
"Arc overloads function calling on tables, lists, strings, etc. So why couldn't they have done that with Racket?"
That page tries to demonstrate how Racket's 'syntax-parse and syntax classes make it possible to write macros with readable implementations and good, consistent error messages. The example it uses is a reimplementation of 'let.
In my own opinion, what that page actually demonstrates is how a misplaced emphasis on readable implementations (code-as-the-spec) can lead to more complexity and less readable code overall. :-p
But to be fair, for lots of people (especially people who are learning programming, a big target audience of Racket), the better error messages improve the maintenance experience much more than verbosity detracts from it. I occasionally ask random not-necessarily-programmer people what they want in a programming language, just to get some ideas, and the second most common response is "Better error messages." (The most common response is "How should I know?!" :-p )
---
"Besides, I'm talking about the `let` in Scheme, so the whole "it makes sense in Racket!" thing doesn't really apply. :P How does it make sense in Scheme?"
Beats me. :) IMO, Arc simply makes more sense than Scheme, and that's fine since Scheme came first.
"Depending on the context, the idea of a hash table with a prototype may be a contradiction in terms. For instance, if by "hash table" we refer to exactly the hash table implementation Racket has today, then it simply has no prototypes. :-p
There are contexts where it does make sense, particularly when talking about hypothetical languages, but if a single language tried to make sense of every idea people had for it, it'd be pretty darn bloated. ^_^"
How so? JavaScript and Lua both add prototypes to tables/objects. It's possible to implement it in Arc right now, it's just not possible to make them blend seamlessly into the language, which is one thing I plan to fix with Arubic.
A prototype isn't defined by it's implementation, it's defined by it's idea. You could define an alist or plist to be a prototype, or an AVL tree, or who knows what else. Heck, I was using functions to represent prototypes earlier. :P There are only two requirements for prototypes in my mind: message passing, and, when data is not found in the data structure, it will check it's parent rather than failing[1].
To put it another way, you can use delegation and message passing to implement the concept of prototypes. And since we're talking about Arubic here, I can implement prototypes with just about any compound data type. :P
So when I mentioned prototypes, I was referring to the fact that it is indeed possible to give inheritance to hash tables, and so although you're right that structs are not the same as hash tables (at least in Racket), I'm pointing out that they could be the same, which is the direction I'm going in Arubic.
Actually, you could probably treat hash tables as prototypes in Racket too, but you would need to use special functions to access them:
(proto-get foo 'bar)
(proto-set foo 'bar 'qux)
---
"Actually, they do. A Racket struct can implement the 'prop:procedure structure type property to specify how it should behave when called like a procedure.
In fact, it can also implement 'prop:dict to behave like a dictionary. Hash tables and association lists are other things that can be used as dictionaries."
Ah, I see, so it's basically a bigger and more complicated version of the message passing in Arubic. I still prefer Arubic. :P
---
* [1]: Technically speaking, you could implement prototypes by copying the data, rather than using delegation... but when I refer to prototypes, it is referring to the delegation-based model, as that's what JavaScript and Lua use.
Also, I do draw a distinction between classes and prototypes, even though both use message passing and inheritance. The distinction isn't a very huge one, though. Instances inherit from a class, and a class inherits from other classes. Interestingly enough, it's possible to emulate classes using prototypes, but not possible to emulate prototypes using (static) classes... thus you could say that prototypes are a superset of classes.
It is possible to emulate prototypes in a language with dynamic classes though, like Python. It just requires you to create a new class at runtime, then create a new instance. So every class would have precisely one instance. This is still kinda kludgy, though... for instance, Python requires some special properties to be defined on classes... you can't add them to instances.