Arc Forumnew | comments | leaders | submitlogin
Ask Arc: shall we redefine nil to () in Anarki? (
3 points by akkartik 1997 days ago | 28 comments

4 points by shawn 1997 days ago | link

Arguably it's an interesting failed experiment. But unfortunately that was not the conclusion Aristotle's successors derived from works like the Metaphysics. [9] Soon after, the western world fell on intellectual hard times. Instead of version 1s to be superseded, the works of Plato and Aristotle became revered texts to be mastered and discussed. And so things remained for a shockingly long time. It was not till around 1600 (in Europe, where the center of gravity had shifted by then) that one found people confident enough to treat Aristotle's work as a catalog of mistakes. And even then they rarely said so outright.

If Arc is to attain widespread adoption – an aim we're focusing on with laarc – I believe that it will need to transition away from the rigid abstractions of the past and give users what they want: true, false, empty list, and a value that means "undefined". Lua calls this nil.

JS also has a value called null. It's very useful to use null as a placeholder. Without null, it's probably impossible to differentiate between calling an arrow function without arguments vs passing in `undefined`. This gave the language flexibility going forward, and gave it an escape hatch that Lua doesn't have: the ability to use a null value as a sentinel value. You'll notice it's quite impossible to store nil in tables in Lua. (It's ... not strictly impossible, but it makes interop with libraries a nightmare.)

Users need JSON. JSON has true, false, null, strings, numbers, arrays, tables, and empty arrays.

Users also want to work with existing libraries. They don't want FFI wrappers. There's just no time for any of that when you're in a mad dash for hammering out features. It's partly why pg gave up on working on the core language and focused solely on HN for some years.

To that end, I propose several changes:

- the result of a predicate is either false or something other than false. This gives us interop with Scheme, because that means we can e.g. pass (fn (a b) (< a.0 b.1)) to racket's SORT function as the comparator, and everything will work fine. We don't get this now, because of the t/nil constraints.

- The result of (if false 'foo) is void, not false. This gives users an explicit way to avoid boolean values in the cases that it matters. Which is rarely, but those rare cases can be excruciating without this.

- The value of the symbol t is changed to #t. This works fine out of the box right now.

- The value of the symbol nil becomes void, not an empty list. (I need to research this one, but I am ~70% confident it can work.)

- car, cdr, and cons are updated to work with the above. I have already done this in a fork of laarc and know this is the key that makes all the other pieces fall in place.

There are many advantages of this model. One is that you'll get the entire racket ecosystem basically for free. It can be up to users whether to rely on racket or whether to stick with pure Arc. (Arc itself should probably stay away from using Racket so as to ease porting the language, but it's a blurred line to decide what counts as "core arc" vs "libraries supporting arc". That said, users need Racket features, and need to be able to evaluate racket code on demand to use these features.)

Another advantage is keyword arguments. This is one of my primary areas of research with respect to arc, and I have a prototype version that handles common cases. It will take some legwork to handle all of the cases correctly, but this gives users the ability to at least call racket functions via keyword arguments without having to resort to #:foo syntax. (#:foo was an aesthetic mistake; I think the original paper basically says "We polled some users and no one had strong feelings, so we went with this." Good luck typing #:foo into your phone's Arc REPL in under two seconds.

Speaking of Racket's SORT:

  (def sorted (xs (o scorer <) (o :key idfn))
    ((seval 'sort) xs scorer key: key))
In the above example, :key is a keyword argument. You can call sorted like this:

  > (sorted '(c a b))
  (a b c)

  > (sorted '(c a b) >)
  (c b a)
  > (sorted '((c 1) (a 0) (b 2)) key: car)
  ((a 0) (b 2) (c 1))
This gives a way to port a huge quantity of Arc code in news.arc from the old style to the new keyword style incrementally. You can transition each argument over to be a keyword when it makes sense to do so. This sort of gradual evolutionary path is important when introducing fundamental changes.

So, that's my case for this. The only closing point I'll make is that shows that all of the above ideas can work without sacrificing purity. It's quite the opposite: you'll find arc to be far more powerful, because you can even transpile it to JS when you need to.

There is no way to protect users from the fact that if they want to transpile, or they want to interop with existing libraries, and users have no language for expressing the semantics of those interfaces, then those users are mostly screwed and will quietly switch to something more effective.

Arc is the embodiment of an exciting idea: that a modern lisp can be fun, and so powerful that you can run circles around competitors before they understand what's happening. (The "suggest title" feature in laarc is still my favorite thing to point to vs HN.) And the core of that mindset is exploration. If this seems like a good idea, we should transition to it. Choosing to roll back later is always an option, even if it would be a little painful.

But I think we'll find that these features make it possible to handle all of the existing cases in Arc without problems, that things tend to "just work by accident" instead of forcing the user to think through unexpected problems, and that you'll be able to write and ship features very quickly -- and perhaps even to more runtimes than just Racket, like in-browser support for the Arc compiler. You'll notice is basically the Arc library, even though it's illustrating functions from Lumen. (whenlet x 42 (prn x)) works, for example.


3 points by krapp 1997 days ago | link

IMH and unqualified O, I think that what holds back widespread Arc adoption (other than the existence of Clojure) are lack of effective namespacing and unhygienic macros, which make modular code and things like proper package management/libraries infeasible if not impossible. Also, the way the dependencies in news are engineered make it very difficult to disentangle or update what has become an obsolescent web application from the core language without risking breaking everyone else's code.

Ironically I've found (others may feel differently) that while pg may have wanted the purpose of Arc to be exploratory programming, he seems to have done so with a number of his own assumptions baked in to the language, implicitly limiting exploration to what he considers to be correct, and to what correlates with his personal style. It's like Arc is Henry Ford's Model T: you can have it in any color you like, as long as you like black.

But changing these aspects of Arc would make it no longer Arc, at least philosophically.


1 point by i4cu 1997 days ago | link

I agree with some of your first point, but the second not so much. I think pg released arc as a first cut with one application baked in to act as marker for a stable version. Obviously I can only guess, but I also think he expected a larger community of interest, one that could take it to the next level. That never happened and with only a handful of keeners, 10 years later, the things you mention in your first point are non-existent or half-baked experiments. It's possible Arc may get there, and I hope it does, but at this rate it may take a hundred years. :)

More interestingly, though, shawn is bringing some interest back (at least for me) and making substantial changes that could breathe new life into Arc. I don't agree with the empty list - nil change, but the table changes and reader changes are good. I do think the more seamless the racket interop is and the more racket can be leveraged, the better. Clojure has good interop with java and that's what made Clojure explosive. If we can do that with Arc/Racket then we are better off for it.


2 points by rocketnia 1996 days ago | link

"Clojure has good interop with java and that's what made Clojure explosive. If we can do that with Arc/Racket then we are better off for it."

Do we ever expect Anarki values to be somehow better than Racket values are? If so, then they shouldn't be the same values. (The occasional "interop headaches" are a symptom of Anarki values being more interchangeable with Racket values than they should be, giving people false hope that they'll be interchangeable all the time.)

I think this is why Arc originally tossed out Racket's macro system, its structure type system, and its module system. Arc macros, values, and libraries could potentially be better than Racket's, somehow, someday. If they didn't already have a better module system in mind, then maybe they were just optimistic that experimentation would get them there.

Maybe that's a failed experiment, especially in Anarki where we've had years to form consensus on better systems than Racket's, and aligning the language with Racket is for the best.

But I have a related but different experience with Cene; I have more concrete reasons to break interop there.

I'm building Cene largely because no other language has the kind of extensibility I want, even the languages I'm implementing it in. So it's not a surprise that Cene's modules aren't going to be able to interoperate with Racket's modules (much less JavaScript's modules) as peers. And since the design of user-defined macros and user-defined types ties into the design of the modules they're defined in, Cene can't really reuse Racket's macro system or first-class values either.


2 points by i4cu 1996 days ago | link

My comment is only a remark to "what holds back widespread Arc adoption".

If your goal is for arc to have widespread adoption then being able to leverage racket in a meaningful way will help get you there.

Currently the ability to drop into racket is not getting people to use arc, it still seems people would rather just use racket.

IMO, It would be better if arc had implicit methods that provide access to racket capabilities. In Clojure having libraries, name spaces, and a seamless interfaces to java translated into a plethora of libraries for Clojurians to utilize. Can we not do the same? Well if the goal is for "widespread adoption" then we need to.


2 points by kinnard 1996 days ago | link

Has anyone tried to pull off a module + package system with unhygienic macros?


3 points by rocketnia 1996 days ago | link

Isn't Common Lisp a language with a package system and unhygienic macros?

Common Lisp's approach is that the way a symbol is read incorporates information about the current namespace. That way usually all symbols, even quoted ones, can only have collisions if they have collisions within the same file, and this makes hygiene problems easier to debug on a per-file basis.

I don't think it's my favorite approach, but it could very well be a viable approach for Arc. I was using an approach somewhat like this in Lathe's namespace system, although instead of qualifying symbols at read time, I was qualifying each of them individually as needed, using Arc macros.


2 points by rocketnia 1996 days ago | link

Oh right, Clojure has unhygienic macros too. Clojure symbols also have namespaces as in Common Lisp.


3 points by krapp 1996 days ago | link

I guess the problem isn't the unhygienic macros, per se, but unhygienic macros and lack of namespaces.

Do you think it would be possible to get ns.arc to work with macros and feasible to add it to the core language?


2 points by rocketnia 1995 days ago | link

Good question, but ns.arc manipulates what Racket calls namespaces, which are data structures that carry certain state and variable bindings we might usually think of as "global," particularly the definitions of top-level variables.

What Common Lisp and Clojure call namespaces are like prefixes that get prepended to every symbol in a file, changing them from unqualified names into qualified names.

I think namespaces are a fine approach for Arc. If Anarki's going to have both, it's probably best to rename Anarki's interactions with Racket namespaces (like in ns.arc) so they're called "environments" or something, to reduce confusion. I think they will essentially fit the role of what Common Lisp calls environments.

Of course, people doing Racket interop will still need to know they're called namespaces on the Racket side. Is there another name we can use for Common Lisp style namespaces? "Qualifications" seems like it could work.


3 points by i4cu 1995 days ago | link

> Is there another name we can use for Common Lisp style namespaces?

realms ?

going for brevity here :)


2 points by krapp 1996 days ago | link

I don't know, but I can't see how it would be feasible when any module or package could arbitrarily and globally redefine existing symbols, functions, operators, etc.


3 points by kinnard 1996 days ago | link

I tried to build a package manager for arc it's partially implemented.


1 point by krapp 1996 days ago | link

How did you define a 'package' in Arc?


1 point by kinnard 1995 days ago | link

I haven't yet really, as yet it's just a set of files of code. After I make some headway on my current projects I plan to turn my attention back to it, they'll each be packages.


4 points by akkartik 1997 days ago | link

Immediate reaction: that's going a bit too far. Racket has `#<void>`, and its absence is one reason I prefer Arc to Racket.

Will the empty list be false-y? I kinda like that it is.

Both these ideas help make programs terser in Arc. Here's a definition that uses both:

    (def map1 (f xs)
    "Returns a list containing the result of function 'f' applied to every element of 'xs'."
      (if xs
        (cons (f car.xs)
              (map1 f cdr.xs))))
I like keyword arguments too, but I don't follow what your preceding proposals have to do with it.

You know what language has perfect interop with Racket? Racket ^_^


2 points by shawn 1997 days ago | link

cdr basically ends up being:

  (define (xcdr x)
    (if (pair? x) (cdr x) ar-nil))
and ar-nil is falsy. So your example will work unmodified.

... oh. And now that I check, you're right about void:

  arc> (seval '(if (void) 1 2))
I foolishly assumed that (void) in racket is falsy. But it's truthy. That rules out using racket's (void). `(null ? 1 : 2)` gives 2 in JS, and `if nil then 1 else 2 end` gives 2 in Lua, so it's surprising that `(if (void) 1 2)` gives 1 in Racket.

For what it's worth, in an experimental version, using #f for ar-nil and #t for ar-t worked. It's a bit of a strange idea, but it helps interop significantly due to being able to pass arc predicates right into racket.

It'd be better for me to show a working prototype with ar-nil set to #f rather than try to argue in favor of it here. But to your original question: yes, anything other than |nil| would be great, since that gets rid of the majority of interop headaches.

One thing that might be worth pointing out: The lack of void means it's back to the old question of "how do you express {"a": false, "b": []} in arc?" Choosing between #f and () for nil implies a choice between forcing {"a": false} or {"b": []} to be the only possible hash table structures, since one of them would be excluded from hash tables. But that might be a tangent.

Yes, the keyword section was poorly explained. My comment should have been prefixed with "some thoughts on arc, in case it's helpful" rather than "here is a proposal." And then I should have taken that comment and put it in a different thread, since keyword arguments are unrelated to the question of nil becoming (). I was mostly just excited for the possibility of leveraging more racket now that denil/niltree might be cut soon.


4 points by krapp 1997 days ago | link

But why should an empty list be falsy? An empty list can be as valid a form of list as a non-empty one. It also seems to me that an empty list shouldn't be nil, since to me, nil should mean "undefined", and an empty list is well defined as an empty list.

Would disambiguation here really make Arc programs less terse? Is that a decision that should be enforced by the language or left to the author?


3 points by akkartik 1996 days ago | link

In my example above, making an empty list truthy would cause this change:

    (def map1 (f xs)
    "Returns a list containing the result of function 'f' applied to every element of 'xs'."
   -  (if xs
   +  (if (~empty? xs)
        (cons (f car.xs)
              (map1 f cdr.xs))))
We can argue how important this is, but disambiguation does definitely make Arc programs less terse.


3 points by shawn 1996 days ago | link

Two points.

- the assumption baked into this argument is that cdr of an empty list returns an empty list. Switching nil to #f and letting empty list be truthy avoids this problem.

- Good names are important. ~empty? isn't really a fair characterization. Lumen uses (some? xs). There is also another way: Update `no` to be (or (is x nil) (empty x)), and then use (~no xs).


2 points by akkartik 1996 days ago | link

First option makes sense.

Second option, part a: I actually find `~empty?` clearer than `some` in this case. Also `some` means something different in Arc.

Second option, part b: wait, then `(if xs ...)` would sometimes not do the opposite of `(if (no xs) ...)`!


2 points by rocketnia 1996 days ago | link

For what it's worth, my approach here is pattern-matching. In Lathe Comforts for Racket I implement a macro `expect` which expands to Racket's `match` like so:

  (expect subject pattern else
  (match subject
    [pattern then]
    [_ else])
If Arc came with a similar pattern-matching DSL and `expect`, we could write this:

  (def map1 (f xs)
    (expect xs (cons x xs) ()
      (cons (f x) (map1 f xs))))
The line "expect xs (cons x xs) ()" conveys "If xs isn't a cons cell, finish with an empty list. Otherwise, proceed with x and xs bound to its car and cdr."


1 point by i4cu 1997 days ago | link

I agree; an empty list is a value. And when you consider interop with other langs, they will infer it to be some object too, where predicates will see it as a value not the lack of one.


2 points by hjek 1996 days ago | link

I actually like `#<void>`, because it makes more of a distinction between pure and impure functions.

I read a good blog post[0] recently on how not distinguishing makes it difficult to guess the behaviour of simple and short code snippets (in JavaScript, but the same could apply to Arc).



1 point by akkartik 1996 days ago | link

There's definitely a tension between being a concise language and being a safe language. Arc doesn't try to help newcomers avoid simple mistakes. It gives them enough rope to hang themselves, like with unhygienic macros. That's partly why I stopped using Arc to teach my students programming (


2 points by kinnard 1997 days ago | link

This is off-topic but pg is shockingly Eurocentric, he seems from that excerpt to be completely oblivious to or completely willing to elide the generations of advances made in almost every field by the luminaries of the muslim empires including things like . . . ALGEBRA!


1 point by i4cu 1996 days ago | link

> Users need JSON. JSON has true, false, null, strings, numbers, arrays, tables, and empty arrays.

JSON supports empty lists (arrays) and 'null'. Currently I can pass in () for the array and nil for null. How would this be managed?


4 points by i4cu 1996 days ago | link (Why should zero be truthy?)

tangentially related with some overlapping content.