"I think I don't understand why object reference is worth a special operator (besides performance reasons)."
The point of "egal" is that you don't have a special operator: you use the same operator for both mutable and immutable objects. It's wart (and Arc/Common Lisp/Scheme) that has a special operator for reference purposes. Just because you call it "addr" rather than "eq" doesn't make it any less of a special operator. And I doubt you put it into wart for performance reasons.
The "egal" article actually defines equality as "operational equivalence":
Two objects are "operationally equivalent" if and only if there is no way
that they can be distinguished, using ... primitives other than [equality
primitives]. It is guaranteed that objects maintain their operational
identity despite being named by variables or fetched from or stored into
data structures. [Rees86,6.2]
In other words, it is possible to distinguish two mutable objects by mutating one of them and checking to see if the same change occurs in the other object. But this is not possible with immutable objects.
So my stance is simple: immutable objects should be compared by recursing on their components, like how "iso" works in Arc. Mutable objects should compare by reference equality, because the whole point of mutation is that you can change the object without affecting other objects which have the same subcomponents. That has to do with the conceptual model of how the objects are used and has nothing to do with performance.
The reason the article talks about performance at all is because immutability is much preferred in parellel computing, and most Lisps treat cons cells as immutable, but they're not actually immutable, so the parallel system has to have extra infrastructure to support mutable conses, even though they're almost never mutated. In any case, the point the article is making is valid even if you ignore performance:
There are a several problems with EQUAL. First, it may diverge in the
presence of directed cycles (loops) in one of its arguments, although some
(e.g., [Pacini78]) have suggested more sophisticated predicates capable of
detecting such cycles. Secondly, it is not referentially transparent; two
calls to EQUAL on the "same" (i.e., EQ) arguments can produce different
results at different times. Neither of these possibilities is to be desired
in a programming language primitive equality test because we would like such
a test to always return and we would like object identity to be preserved.
Yet EQUAL is an extremely valuable operation, because the vast majority of
Lisp lists are side-effect free--i.e., "pure". Without side-effects, loops
cannot be constructed and sublists cannot be modified, and within these
restrictions EQUAL becomes a well-defined and useful operation.[8]
This tension between the simplicity of EQ and the usefulness of EQUAL has
led to a great deal of confusion. This confusion has now lasted for 30
years, with additional confusion having been recently added by Common Lisp.
Since neither EQ nor EQUAL has the desired semantics for the multiplicity of
Common Lisp datatypes, Common Lisp added six more--EQL, EQUALP, =, CHAR=,
STRING=, and STRING-EQUAL, yet these also lack the correct semantics.[9]
Scheme continues this confusion by offering the three predicates (eq?, eqv?
and equal?) which are roughly equivalent to Common Lisp's EQ, EQL and
EQUALP.
In other words, if you have mutable cons cells that are almost always treated as immutable, you need both eq and equal. But if you create two datatypes: mutable cons and immutable cons, then you only need "egal". With immutable conses, you don't want to use "eq" on them because they're functional: letting you grab the reference of an immutable cons would be an abstraction leak.
But with mutable conses, you do need "eq", because two cons cells that are equal may not be eq. So, if your language distinguishes between mutable and immutable conses, you only need "egal". This not only prevents abstraction leaks, but it also preserves operational equivalence, which means that if "egal" says that two objects are equal, then it's impossible to distinguish them in any way. This is not true with eq/equal.
As a practical example of why you would want that... look at Racket. It has three kinds of hash tables: hash-eq, hash-equal, and hash-eqv. And it has all kinds of issues if you use a mutable object as a key in an "equal" hash:
Caveat concerning mutable keys: If a key in an equal?-based hash table is
mutated (e.g., a key string is modified with string-set!), then the hash
table’s behavior for insertion and lookup operations becomes unpredictable.
So you, the programmer, have to decide every time you create a hash table which kind of equality it should use. And if you choose wrong, you end up with issues. And it also means you can't mix mutable and immutable keys in a hash table. This is ridiculous. With "egal" this isn't a problem.
Notice that none of this has to do with performance. It has to do with the conceptual model of how objects behave.
I wasn't able to post this earlier, due to the Arc Forum being all wonky, so I'll post it now.
---
"I'm pretty sure those two literals turn into objects with different 'identities'."
Weird, I distinctly remember Python returning false for that... I guess I was wrong about == in Python/Ruby.
Oh, I see, I was thinking about the "is" operator, which does return False, but is rarely used. So you were right: Python uses equal by default for arrays/dictionaries. However, that only applies to the built-in types:
>>> class Foo:
... pass
>>> Foo() == Foo()
False
User-created types can add an __eq__ method to implement "equal" testing, but it's not the default.
JavaScript, however, uses object reference for both "==" and "===":
[1, 2, 3] == [1, 2, 3]
[1, 2, 3] === [1, 2, 3]
Which, as far as I'm concerned, is the correct semantic, because arrays are mutable.
"What I meant was that the library developer wants our applications to use their new library version without any involvement of the application developers. I seriously doubt that's what you're claiming to support."
I don't get the impression they're saying, "we release this new library and all the users of the existing libraries automatically switch to it". I think what they're saying is, "this library can be used by existing applications, but they still need to choose to use it". That's how I interpreted the "drop-in replacement" phrase.
Technically speaking Nulan can support that... you would change library1 and library2 to just import library3:
Then the applications don't need to change at all. That's assuming of course that the developers of library3 can change library1 and library2. If they can't, then oh well, my solution is for the applications to manually update to use library3 if they so wish. In that semi-contrived example it doesn't matter because library3 is a superset of library1 and library2, so applications can just keep using library1/2 just fine.
---
"I think the only place we really disagree now is that you're content to neglect the "RubyGem" parts of these challenges for the purposes this discussion, while I consider them the hardest parts. I think if you do try to design a package manager, you may very well get most of a namespace system out of the deal, or even a whole programming language."
They are the hardest parts, I just don't think it has to do with namespace support, which that entire topic was devoted to. Nor am I particularly interested in designing a package manager.
So my suggestion still stands: if you feel this strongly about package managers, make up your own semi-contrived challenges that try to demonstrate what a package manager should be capable of doing. I'm sure it will include all kinds of stuff that was left out of the namespace challenges, because package managers are much more complex and involved than namespaces.
Then you can point to those challenges and I can say, "Nulan doesn't support that stuff" rather than trying to insist that Nulan is failing the namespace challenges because you're interpreting them far larger than what they actually said. I think it's useful to have "package manager challenges", but I think it's also useful to have "namespace challenges". I don't see why we need to throw out the namespace challenges just because you view it as "not as hard" as package managers.
---
"The challenge we're talking about puts the words "different, incompatible versions" in italics. If direct substitution were unimportant, the challenge would not even need to use the word "version"; it could just say there were two separate libraries with name collisions. And yet that's precisely the scenario of the previous challenge."
Like I said, I don't get the impression they're talking about package managers. I think they used the phrase "different incompatible versions" because that's a situation that can actually occur in real life. They're using it as an example, that's all.
It's obvious that you want to think in terms of package managers, and that's fine, but I really do not get the impression that's what they're talking about. Ultimately, it seems the only way to resolve this is to actually ask them what they meant.
"The challenge seems to imply that the two incompatible libraries were loaded successfully: it's NOT talking about file resolution. It's assuming the file resolution proceeded successfully, and thus the only issue remaining is resolving the name collisions."
From what you quoted, technically that could be true.
---
"You keep talking about things like dependency resolution systems, swapping in different libraries, etc. That's fine, but that does not seem to be what the challenges are talking about."
Objection! :)
The challenge we're talking about puts the words "different, incompatible versions" in italics. If direct substitution were unimportant, the challenge would not even need to use the word "version"; it could just say there were two separate libraries with name collisions. And yet that's precisely the scenario of the previous challenge.
Me: "This challenge is a case where the library developers are trying to change their code without causing disruption for anyone else."
You: "And Nulan's namespace system handles that just fine."
What I meant was that the library developer wants our applications to use their new library version without any involvement of the application developers. I seriously doubt that's what you're claiming to support.
---
"You could argue that's a good case for a package manager like RubyGem, and that may be true, but I consider such a system to be in a layer above the namespace system."
I think the only place we really disagree now is that you're content to neglect the "RubyGem" parts of these challenges for the purposes this discussion, while I consider them the hardest parts. I think if you do try to design a package manager, you may very well get most of a namespace system out of the deal, or even a whole programming language.
Unfortunately, package managers and filesystems aren't quite a full layer above the languages they're used with. Program units tend to link together using hardcoded filenames or package names, a practice that ties some of the filesystem or package manager complexity into the complexity of the language proper.
The module system I've been working on recently (ever since [1]) is meant to help resolve that, by making the non-language-specific complexity as simple and universal as possible. It's based on two notions: A program is a multiset of independently maintained extensions, and it exists in the context of an expansive historical record containing all knowledge ever discovered (or at least all knowledge we have on hand).
The extensions and units of background knowledge may ultimately be encoded as files and managed at a "layer above" using traditional package managers and filesystems. However, they may also be manipulated as first-class values in a host program, or even as visual elements in a UI.
irb(main):001:0> class A
irb(main):002:1> end
=> nil
irb(main):003:0> a = A.new
=> #<A:0x8f6dd3c>
irb(main):004:0> a == a
=> true
irb(main):005:0> a == a.clone
=> false
irb(main):006:0> x = "abc"
=> "abc"
irb(main):007:0> x == x
=> true
irb(main):008:0> x == x.clone
=> true
irb(main):009:0> (1..10) == 10
=> false
irb(main):010:0> (1..10) === 10
=> true
- Perl (near as I can tell) doesn't have anything in the way of user-definable comparison, but still has separate operators. Granted, I don't think the separation is related to the reference-vs-value thing---I don't do Perl much, so the semantics confuse me:
It'll be hard to appeal to the authority/popularity of most languages, because equality is a tricky concept with many meanings---whether or not it's specifically the address-vs-value (eq-vs-equal) thing.
I like egal fine if we decide to tell the type system about immutability, but that seems like a major design decision. I'm not sure this one feature is sufficient reason.
I don't understand the paper very well (thanks for the pointer). I'm not sure why he cares about identity so much if most of the time we're treating lists as immutable. And if a list is mutable equality is not forever, all it returns is whether these two lists are equal right now.
The only new drawback I learned about was that equal can't handle cycles. I hadn't thought of that. It's not hard to fix, but the obvious fix costs performance every single time.
I'm going to go reread it, but so far I think it suffers from being written 10 moore's law generations ago, and complects performance considerations with API design.
"Most languages have only eq and no equal: the == operator in Ruby/Python compares by object reference."
$ python
>>> [0, 1, 2] == [0, 1, 2]
True
I'm pretty sure those two literals turn into objects with different 'identities'.
Most of the time I don't care what 'identity' an object has. If I'm doing weird stuff with assignment and mutable structures I'm happy for the language to push back and make me go back to the straight and narrow asap.
"My pet peeve: Lisp's multiple flavors of equality are still with us 50 years later, leading later languages like javascript astray. The superior approach IMO is to use structural equality by default, and offload the semantics of eq to an operator that returns a stable address for a value. In my toy language I call this operator addr. So I'd replace (eq a b) in Common Lisp with something like (equal (addr a) (addr b))."
Nulan, of course, uses an "egal" I wrote for Racket:
; Generic equality predicate, based on egal
; http://home.pipeline.com/~hbaker1/ObjectIdentity.html
(define (is? x y)
(if (number? x)
(if (number? y)
(= x y)
#f)
(if (immutable? x)
; TODO: do I need to check if y is immutable too?
(equal? x y)
; need to use eqv? for characters
(eqv? x y))))
In Nulan it's called "is".
The reason multiple equalities came about is because cons cells were mutable, but were treated like as if they were immutable: thus, most of the time you want to use equal on them, but occasionally you need to use eq.
If you instead have immutable conses and mutable conses, "egal" can correctly distinguish between them. Thus the problem is that we have a datastructure (cons) that attempts to both be mutable and immutable at the same time.
The solution is to properly separate mutability from immutability.
---
"Most programmers don't actually use eq that often. Python and ruby and perl still don't have it. No vaguely insulting muttering has resulted. (Is it really a big enough inconvenience to cause even muttered insult?) Most of the time eq is just an optimization, usually a premature optimization, and even otherwise one that can mostly be delegated to the implementation."
Actually, that's incorrect. Most languages have only eq and no equal: the "==" operator in Ruby/Python compares by object reference.
The reason they don't complain about it is because they never use "equal" because in those languages, every object is mutable.
But in Lisps, we treat cons cells as immutable, hence the issues with eq vs equal. So the reason they don't complain in Ruby/Python is simply because their objects are always mutable, thus eq is always the correct operator.
"In that case, this has nothing to do with name resolution, it has to do with file resolution: finding which file to import when given a name."
As an argument in favor of this viewpoint, I'll quote the original challenge:
"Now I'm writing a piece of code that uses two libraries, each of which use different, incompatible versions of some third library, causing namespace collisions between the two versions. How does the namespace system handle this?"
The challenge seems to imply that the two incompatible libraries were loaded successfully: it's NOT talking about file resolution. It's assuming the file resolution proceeded successfully, and thus the only issue remaining is resolving the name collisions.
You keep talking about things like dependency resolution systems, swapping in different libraries, etc. That's fine, but that does not seem to be what the challenges are talking about. I'd rather see some challenges specifically geared toward dependency resolution and file loading rather than trying to hijack namespace challenges, since I see these two issues as being orthogonal, with maybe a little overlap.
"I'm assuming the example is supposed to look like this instead"
Well, that either won't work (it would load the same "library3" in both cases), or it would work with a sufficiently smart lookup algorithm.
Basically, when importing a file, I think it should look for it in this order: the cwd, the folder (and subfolders) where the importee is defined, then lastly the folder where Nulan itself is defined.
In that case, assuming library1 and library2 are in different folders and have different versions of library3 in their path, it would work.
This implies bundling the libraries you use with your app, which you pretty much have to do anyways.
In that case, this has nothing to do with name resolution, it has to do with file resolution: finding which file to import when given a name.
You could argue that's a good case for a package manager like RubyGem, and that may be true, but I consider such a system to be in a layer above the namespace system.
---
"This challenge is a case where the library developers are trying to change their code without causing disruption for anyone else."
And Nulan's namespace system handles that just fine.
---
"I think the point is that the library has been upgraded, and the library developer would like the upgrade to propagate to all code that uses the library, even though a few library users might actually prefer otherwise. This is a conflict of preferences, and the language might give tools to certain participants to "reconcile" this conflict."
I still don't get the impression they were talking about some sort of package management or version dependency resolution system. It's certainly okay to discuss such things, but I think they're outside the scope of a namespace system. That thread (and post) seemed to me to be talking specifically about resolving naming conflicts.
I think the talk about "multiple libraries" and "multiple versions" was just to create a situation where naming conflicts occur. I don't think it was meant to imply that the goal is to be able to easily change library versions on the fly, like you were discussing.
If I wanted some sort of system like that in Nulan, it wouldn't be particularly hard to do... you'd create a new file, like say... "dependencies.nu" and you'd then put a bunch of imports into it:
(import foo)
(import bar)
(import qux)
That's all it would be: a list of imports. Then all the dependencies are in one place, rather than scattered around in various files. I personally don't see much benefit to that, but you could do it if you wanted to.
---
"However, I think Casey McCann is assuming a namespace system built on implicit name prefixes, which may or may not incorporate a way to take a name prefix and find a module that addresses it."
That is quite possible.
---
"By "nothing at all," do you mean a late-bound global scope like Arc's?"
Yes. And JavaScript. Basically any language with dynamic scope at the global level by default.
If you don't identify as a programmer, if that isn't your core strength, if you just program now and then because it's expedient, then treating libraries as services may make more sense. If a major issue pops up you'll need to find more expert help, but you knew that already.
I did. :)
Even so, I think it's also worth considering that libraries also represent learning opportunities for some (like myself). For whatever reason, perhaps because it isn't my core strength, the only approach to programming that I have found to work for me is to continually try to do something. I seem only to be able to truly absorb the finer points when they are encountered in the context of a whole.
For a long time this kept me from programming altogether.
"In library1's lexical scope it will use library3-version1, and in library2's lexical scope, it will use library3-version2."
I'm assuming the example is supposed to look like this instead:
; library1 (coded with library3's version 1 in mind)
(import library3)
; library2 (coded with library3's version 2 in mind)
(import library3)
Each of these libraries would work with a certain library3, but library1 might not work with the newest version, or (in my dream world where everything is perfectly forward-compatible) maybe library1 is just considerably more performant with the old version.
---
"You seem to be suggesting some mechanism in which a module can actually modify what a different module imports and exports. I fail to see the benefit in that: that module's code is written under the assumption that its imports won't be changing. Could you provide an example where that would be useful?"
Here's Casey McCann's full description of the challenge again, just for clarity's sake:
Casey McCann: "At some point, two popular libraries with related functionality get tired of duplicating effort, so they collaborate to produce a third library providing the best of the shared features. To avoid unnecessarily breaking client code with the new versions, they'd like the new library to be able to occupy both original namespaces, or otherwise be easily substituted in. How would the namespace system handle this?"
I had it wrong before, when I made the sweeping statement that only the application's code should have to change in these challenges. This challenge is a case where the library developers are trying to change their code without causing disruption for anyone else.
Ideally, the only thing an application developer should have to do to upgrade is to replace the existing two libraries with the new one. This is even something that an end user might handle using a package manager.
---
"I think what they're asking is: can the original library coexist at the same time as the replacement, even though they have name collisions. Notice they used the word "reconcile". This suggests reconciling the name collisions. There's no need to "reconcile" the other library to use the replacement: it can just keep using the original."
I think the point is that the library has been upgraded, and the library developer would like the upgrade to propagate to all code that uses the library, even though a few library users might actually prefer otherwise. This is a conflict of preferences, and the language might give tools to certain participants to "reconcile" this conflict.
I prefer to view this as a last resort. The module system should give a complete(-feeling) range of freedom to every participant, and the ranges should not overlap. I would allow upstream developers to propagate their changes only as far as downstream developers have opted in. This turns into a design for package versioning.
---
"Nope, I don't think namespace == module."
I agree. The challenges are supposed to be realistic cases where name collisions would actually occur and cause havoc, so naturally they mostly involve programs made up of parts that have been written by multiple people independently. So we should expect to consider modules here, but yeah, the modules don't have to correspond one-to-one with namespaces.
However, I think Casey McCann is assuming a namespace system built on implicit name prefixes, which may or may not incorporate a way to take a name prefix and find a module that addresses it. While this may be a fine way to think of namespaces Java, Racket, and Haskell, the challenges which arise from this assumption may not apply to Nulan. No worries. :)
That said, if you change Nulan's design in order to accommodate some of these other challenges, you may come back to this one and find you've broken it. That is to say, this is still a meaningful test case even if it passes at the moment.
---
"My point is that even if a language doesn't provide include/exclude/import-as, it'd still be better off using hyper-static rather than nothing at all."
By "nothing at all," do you mean a late-bound global scope like Arc's? I think I agree with you.
I fail to see the issue. In library1's lexical scope it will use library3-version1, and in library2's lexical scope, it will use library3-version2. I have no clue what issue you're thinking of.
---
rocketnia: "Regarding "they'd like the new library to be able to occupy both orignal spaces," your code is again incomplete. If the two originals are library1 and library2, an intermediate library library3 depends on library1, and the new one is library4, then how do you get library3 to start using library4 instead of library1? Keep in mind you can't modify any of the libraries."
In other words, you have two libraries. Then they create a third library which merges the two libraries together. Existing code that relies only on library1 will continue to work. Existing code that relies only on library2 will continue to work. New code that wishes to use either library1 or library2 can instead use library3, which combines both library1 and library2.
There's no need to change application1 and application2: they can just keep using library1 and library2. You might say that's inefficient and you'd rather them use library3, but that's up to them whether they want to switch or not. I'm also imagining the situation that library3 actually does cause some backwards compat issues, in which case it would be desirable to keep using library1/library2. The application chooses which libraries to import.
You seem to be suggesting some mechanism in which a module can actually modify what a different module imports and exports. I fail to see the benefit in that: that module's code is written under the assumption that its imports won't be changing. Could you provide an example where that would be useful?
---
rocketnia: "Regarding the licensing example with "they'd like it to be a drop-in replacement [...] An application I'm working on uses two libraries, one of which requires the replacement library [...] The other [...] uses the original basically by default": I think they're asking for a mechanism that lets the application choose whether the "other" library uses the original or the replacement. I don't see how your code makes this possible, and your "this could instead import replacement" comment suggests that you'd actually modify library2."
No I do not think that's what they're asking. I think what they're asking is: can the original library coexist at the same time as the replacement, even though they have name collisions. Notice they used the word "reconcile". This suggests reconciling the name collisions. There's no need to "reconcile" the other library to use the replacement: it can just keep using the original.
But if I'm wrong and they were in fact not talking about name collisions at all but about the orthogonal issue of replacing another module's imports... well, no, Nulan doesn't let you do that, because I don't see any useful benefit from doing so.
In any case, that's a completely different issue from avoiding name collisions, and seems to me like it'd be a part of some sort of build system or dependency resolution system, like RubyGems. That might be useful, but I don't see it as being a part of the namespace system per se.
---
rocketnia: "Regarding "conrol the names you import [...] such that [...] a new export to an existing library will not affect the meaning," it looks like you do have a good response to this. Personally, I wouldn't want there to be any imports that could possibly break that way, but I guess I'm more picky than they are. :)"
Yup, I try to be pretty laid back (at least about my code). If you're anal about it, you can use "include", but I think it's nice to have the option of just doing unqualified imports. It's up to the library/application author, in my opinion.
---
rocketnia: "You missed my point. If the two libraries really use the same namespace, your code for importing both of them will turn into something like this:"
Nope, I don't think namespace == module. You are importing the same module twice, but it's in the same namespace. And if you did this:
(import library1)
(import library2)
You're importing two different modules, but they're in the same namespace. In other words, "library2" can see the stuff exported by "library1", even though it didn't import it.
You might say this is undesirable. I agree it can be undesirable in certain situations, and I think the solution is to import each library in a separate namespace. But hyper-static scope can be implemented with a single namespace, so whether it's single-namespace or multi-namespace is an orthogonal issue.
---
rocketnia: "I often consider what would happen if two versions of a single library were both useful in a single application. If the library author didn't anticipate and avoid self-conflict, the two versions would host a train wreck of collisons on every single name, even the library name. I think that's the kind of challenge Casey McMann has in mind here."
In that case whichever one is defined last "wins", and you can use "include" and "exclude" to control which ones you want. I could also provide a way to import a library into a separate namespace to solve that case:
I think that's useful, but hyper-static can be implemented without it. My point is that even if a language doesn't provide include/exclude/import-as, it'd still be better off using hyper-static rather than nothing at all.
Actually, email supports threaded trees using the References: header. Every message adds the ids of all messages it's under, and it turns out that is enough information to recreate the tree structure. Text-mode email clients like pine and mutt use this.
Given my history with those programs, I'm anal about hitting reply on the precise message in a thread that I'm replying to :) That helps list archives get the tree structure right even though it's invisible over gmail. So in your place I'd have split my message into parts and attached them to the right messages in the thread :) I know, it's utterly ridiculous and irrational.
And here are some replies to the "meta" part of that email exchange:
---
akkartik: "Thanks! I find myself wishing we could put Ross's long response in the 'namespace' section of the page rather than the 'meta' section."
I was conscious of that when I sent that reply. I'm not sure how to reply to a particular "subthread" when it's email. >_<
The only time I've ever seen email become a threaded tree is in mailing list archives, and I kinda assumed that had to do with automatic detection of quotations or something.
---
akkartik: "(I'm not actually expecting you to do this, just thinking aloud about how someone could maintain hygiene when managing a forum.)"
The difficulty here isn't managing one forum, but managing a conversation that spans multiple communication media. Unfortunately, I don't expect there to be a nice bridge that eliminates more complexity than it introduces. That kind of synergy tends to rely on foresight on the part of both systems.
This is an extremely clarifying post. I think it would have headed off a few of the misunderstandings we had over email. XD
Now to reply to the later parts of that email exchange...
---
Me: "Pauan, when you show code examples that demonstrate how you'd deal with certain library dependency corner cases in Nulan, are you considering that the application developer can't go in and change the code of the existing libraries?"
Pauan: "Yes, definitely."
Then I'll respond to your examples individually.
Regarding "two libraries [...] use the same namespace and have name collisons," we've already started to talk about this case, so I'll skip it here and address it below.
Regarding "two libraries, each of which use different, incompatible versions of some third library," the code you posted doesn't comprise a solution. How do you keep library1 and library2 from using the same version of library3?
Regarding "they'd like the new library to be able to occupy both orignal spaces," your code is again incomplete. If the two originals are library1 and library2, an intermediate library library3 depends on library1, and the new one is library4, then how do you get library3 to start using library4 instead of library1? Keep in mind you can't modify any of the libraries.
Regarding the licensing example with "they'd like it to be a drop-in replacement [...] An application I'm working on uses two libraries, one of which requires the replacement library [...] The other [...] uses the original basically by default": I think they're asking for a mechanism that lets the application choose whether the "other" library uses the original or the replacement. I don't see how your code makes this possible, and your "this could instead import replacement" comment suggests that you'd actually modify library2.
Regarding "importing a package should only introduce the names that library explicitly exports," I agree you've got the important parts of this covered.
Regarding "conrol the names you import [...] such that [...] a new export to an existing library will not affect the meaning," it looks like you do have a good response to this. Personally, I wouldn't want there to be any imports that could possibly break that way, but I guess I'm more picky than they are. :)
---
Pauan: "That's the point of lexical scoping: they can use the same names and
not collide with eachother."
You missed my point. If the two libraries really use the same namespace, your code for importing both of them will turn into something like this:
(import library1)
(import library1)
...
Obviously this extrapolation of your code is a degenerate case, and I bet you wouldn't really suggest it. But the challenge itself is degenerate in exactly this way.
I often consider what would happen if two versions of a single library were both useful in a single application. If the library author didn't anticipate and avoid self-conflict, the two versions would host a train wreck of collisons on every single name, even the library name. I think that's the kind of challenge Casey McMann has in mind here.
* [1]: Because I have had a hard time using the Arc Forum lately, akkartik, myself, and rocketnia had an e-mail conversation. I compiled this conversation into an HTML page where it can be read:
rocketnia: "Hmm, actually, my definition of "dynamic" scope might be "not generally possible to associate a usage site with a particular definition site at compile time." I've referred to mutability as a kind of dynamic scope before."
I'm fine with that definition.
---
rocketia: "We're arguing semantics here, but the way I see it, "lexical" means "local to a chunk of code," and "dynamic" means "local to a run time stack frame and its children." "Global," on the other hand, means "local to nothing.""
I'm not a fan of those definitions, in large part because the way I'm implementing Nulan is based on first-class environments, and thus "dynamic" means "evaluated in the environment where the function is run" and "static/lexical" means "evaluated in the environment where the function is defined".
I think that's more useful conceptually, rather than bringing in things like stack frames and a distinction between global/local: Nulan treats global environments exactly the same as local environments.
---
rocketnia: "Racket modules don't provide that feature by default, but the person defining the module does have the option to provide it. This makes it easy for the author of an existing module to introduce mutability without breaking backward compatibility. Since you like immutability by default anyway, I'm surprised you take the opposite stance for module-level variables."
Yes and I don't like having to go through those extra hoops. I also dislike the inconsistency. Which is why Nulan uses purely static environments everywhere, and it lets you assign to variables defined in another module. I think this is simpler to reason about and is more powerful too.
As for immutability... that's not correct. I don't value immutability by default. I value immutability for practical reasons. All of Nulan's datatypes (cons, object, number, environment, etc.) are immutable because I've found that to be remarkably useful. But I've come to the conclusion that making variables mutable by default (like Racket and Arc do) is perfectly fine, from a practical perspective.
And even if I did decide to make variables immutable by default, I'd still need to provide some way to easily create mutable variables in the situations where they're useful. And I'd like to allow for modules to modify these mutable variables even if they're defined in a different module.
You might be thinking, "but what if I want to write a module that doesn't allow for mutation?!" My solution is very simple:
(var foo 5)
... do stuff with foo ...
(const foo foo)
The "var" function creates a mutable variable and the "const" function creates an immutable variable. So, the above first creates a mutable variable, which can be mutated by the module. At the end of the module, it replaces it with an immutable variable, and due to the static global scope, this works out perfectly fine.
In fact, you could make this even stricter and delete the variable entirely, effectively making it private to your module:
(var foo 5)
... do stuff with foo ...
(unvar foo)
And now you have created a global mutable variable "foo" which is private to your module. And once again, this works just fine because the global variable is static. And if you would prefer to not put "unvar" at the end of the module, you could use something like "exclude":
(exclude {foo}
(var foo 5)
... do stuff with foo ...)
I think this is a better tactic than Racket's "you have to jump through some extra hoops to mutate things".
---
akkartik: "I don't understand how a global binding can have a lexical scope if it's not attached to a block. Do you want it attached to a file/module/namespace by default?"
Sorta. In this thread[1] I already explained my views, but to summarize, I think functions should only be able to refer to variables that exist at the time the function is defined. In other words, no late binding of global variables.
Alternatively, you could go with Racket's system which uses lexical scope at the module level, and uses modules for everything by default. That works just fine too. The important thing isn't which particular lexical scope system your language uses, the important thing is that it doesn't use dynamic scope by default.
Hmm, actually, my definition of "dynamic" scope might be "not generally possible to associate a usage site with a particular definition site at compile time." I've referred to mutability as a kind of dynamic scope before.
This definition connects with the "not at compile time" meaning of the word "dynamic," and I wonder if it was one of the main complaints lexical scope advocates had against the status quo. I seem to remember reading about that history somewhere, but I don't have a citation, so I could just be making up rumors. :-p
"No, they certainly aren't. Just like you can have dynamic/lexical function scope, you can have dynamic/lexical global scope. There's no difference between global/local. There is a difference between lexical/dynamic."
We're arguing semantics here, but the way I see it, "lexical" means "local to a chunk of code," and "dynamic" means "local to a run time stack frame and its children." "Global," on the other hand, means "local to nothing."
While I'd put away these definitions and accept yours for the sake of argument, I'm having trouble figuring out what yours even are, and it looks like akkartik is too.
---
"Racket's modules use dynamic binding inside the module, but lexical binding outside."
Huh? If you could explain this one sentence in terms other than "dynamic" and "lexical," maybe that explanation could clarify what your definitions are all on its own. No promises though. ^_^;
---
"And they disallow certain dynamic features that I like, such as assigning to a variable from another module."
Racket modules don't provide that feature by default, but the person defining the module does have the option to provide it. This makes it easy for the author of an existing module to introduce mutability without breaking backward compatibility. Since you like immutability by default anyway, I'm surprised you take the opposite stance for module-level variables.
"I've been taking this approach for granted for months, but no longer recall how unconventional it is."
I don't know whether it's unconventional, but it can be implemented in single-threaded Arc, and I use the name "w/global" where you use "making." If your 'making plays nicely with threads and continuations, then I guess I'd consider it strictly more expressive than Arc, but it's up to you to decide if this expressiveness is helpful.
At one point Clojure supported dynamic binding of module-level variables using (binding ...). For some reason, it looks like they changed this policy so the dynamic binding would only work with variables declared (def ^:dynamic ...).[1] So it looks like Clojure programs now use dynamic scope just as explicitly as Arc programs use Racket parameters, but with a less first-class treatment. I'm not a Clojure expert, so I could be leaving out critical details here.
I don't understand how a global binding can have a lexical scope if it's not attached to a block. Do you want it attached to a file/module/namespace by default?
No, they certainly aren't. Just like you can have dynamic/lexical function scope, you can have dynamic/lexical global scope. There's no difference between global/local. There is a difference between lexical/dynamic.
Most languages (that I know of and use) have lexical scope for functions but dynamic scope for globals. And I've found that all the issues with name collisions are traced back to this global dynamic scope.
If you instead make the global scope lexical, there are no more namespace issues.
"Do you mean allowing globals to be lexically bound?"
I mean that the default binding for globals should be lexical. Arc and JavaScript do not have lexical globals. At all. Ever. It's just not possible.
Racket's modules use dynamic binding inside the module, but lexical binding outside. This is reasonable, but Racket modules are clunky and big and complicated and such. And they disallow certain dynamic features that I like, such as assigning to a variable from another module.
Racket modules are hard to implement, hard to understand (once you get down to the nitty gritty), and they're much too rigid and staticy.
I want something that's simple to implement, simple to understand, concise, flexible and dynamic, no boilerplate, and yet has all the power of Racket's modules. Using lexical global scope + a few utilities can achieve that.
---
"Is it a bad idea?"
I think dynamic scope is both useful and practical. I just don't think it should be the default. So, yes, having a distinction between "let" and "making" is good. But lexical should be the default.
"Of course this problem is completely solved by making the global namespace lexical rather than dynamic."
What do you mean by "lexical"? Module-local?
In Arc, I'd recommend you avoid using global scope for anything local (natch ^_^ ). You may prefer the ability to declare private variables or something, but a big (let ...) isn't that bad. You're even using the equivalent of a big (let ...) in JavaScript.
If what you want is first-class namespace support or selective importing, that's different.
Do you mean allowing globals to be lexically bound? Wart uses different expressions to create a new lexical (let, etc.) vs dynamic (making) binding, so any var can have both bindings, and lexical bindings always beat dynamic ones. I've been taking this approach for granted for months, but no longer recall how unconventional it is. Is it a bad idea?
By the way, in case you're wondering... the %copy signature is this here:
(f x)
Yeah, that tiny little thing. What that does is it takes the %copy method and calls it with the object as its first argument. That's it. As long as that doesn't change, then there's no need to update the objects.
So, if we wanted to add in some new copy functionality that required us to pass more information to the %copy method, we'd need to change the copy function to pass more arguments to the %copy method, which would require changes to all the objects that use the %copy method.
But in this case, we don't need to pass any more arguments to the %copy method so nothing changes except the copy function itself.
And the copy function uses @r which means "0 or more extra arguments", so it works just fine in the 1-argument case. Thus, this change is backwards compatible in every way (that I care about).
"I assume Nulan's way of doing extensible copy would be to call an object's %copy method or something like that. But would it be able to handle if the signature for copy changed, or would you have to go update all the %copy methods?"
Well, there's no need for copy since everything's immutable in Nulan... but assuming I did want copy, yes there would be a %copy method for objects. And yes, if you changed the signature for the %copy method you'd need to change all the objects.
That's because methods form an interface: if you fulfill the interface, you get the functionality. So naturally if the interface changes, any code that uses that interface will need to change. If you're really worried about backwards compat, you could instead create a new %copy2 interface, etc.
But the copy function itself is not the same as the %copy method. So even if the copy function changed, it might not require changes in the %copy method. It depends on what the changes are.
Looking at that function... it looks like you're saying that this:
(copy x 'a 5 'b 10)
Would copy "x", and then assign "a" to 5 and "b" to 10, correct? That change would be trivial to do in Nulan, without affecting the %copy method at all. I guess it'd look something like this:
(def copy -> (let x [ %copy f ]) @r
(let x (f x)
(each (pair r) -> [k v]
(set! x k v))
x))
Not so different from in wart, eh? Though I'd prefer to use an immutable version that doesn't use set! and boxes. Maybe something like this instead:
(def copy -> (let x [ %copy f ]) @r
(foldl (f x) (pair r) -> x [k v]
(set x k v)))