Arc Forumnew | comments | leaders | submit | Pauan's commentslogin

With the current implementation of Arc, it is possible but rather tricky to write hygienic macros.

However, there is a very simple change that can be made to the Arc compiler which would make hygienic macros easy to write.

The Arc compiler would have a big hash table which maps from global symbols to gensyms.

Whenever you define a global variable, it will create a new gensym and store it in the hash table.

When using the quasiquote syntax, it will lookup the variable in the hash table and will use the gensym rather than the symbol.

As an example, if you have this Arc program:

  (= foo 1)

  (mac bar ()
    `(+ foo 2))
The Arc compiler would expand it to this code instead:

  (= #<foo:1> 1)

  (mac #<bar:1> ()
    `(#<+:1> #<foo:1> 2))
In this case, #<foo:1>, #<+:1>, and #<bar:1> are gensyms, rather than ordinary symbols.

Now the macro bar will always expand to the correct global variable foo, and therefore it is 100% hygienic.

If you want to intentionally break hygiene, you can simply do this:

  (mac bar ()
    `(+ ,'foo 2))
The above code uses "," to insert the symbol "foo", rather than the gensym for foo.

This change is very easy to make in the Arc compiler, and it only requires a few minor changes to the language.

-----

3 points by Pauan 3338 days ago | link | parent | on: ASK: is arc used in production?

> static typing seems to preclude true macros

I may be wrong on this, but it seems to me that static typing does not prevent macros at all (true or otherwise).

I switched Nulan to use static typing, yet it's using the same kind of macro system that it used when it was dynamically typed.

As far as I can tell, a macro is simply a compile-time function that accepts code and returns code. If so, then its static type is "Code -> Code".

In Nulan, the Code type might be defined like this:

  (TYPE Code
  | (*integer Integer)
  | (*number Number)
  | (*string String)
  | (*symbol String)
  | (*gensym Integer)
  | (*list (List Code)))
In other words, it can be an integer, number, string, symbol, gensym, or list of Code.

Within the macro's body, you can pattern match on the Code, you can map/filter on it just like in Arc, you can dynamically return Code, etc.

In Nulan, this is made easy with the & syntax, which is just a shorthand for the Code type:

  # These two are equivalent
  &1

  (*integer 1)


  # These two are equivalent
  &(foo 1 2)

  (*list [ (*symbol "foo") (*integer 1) (*integer 2) ])


  # These two are equivalent
  &(foo ~a ~b)

  (*list [ (*symbol "foo") a b ])
And the & syntax works when pattern matching as well:

  # These two are equivalent
  (MATCH foo
  | &(foo 1 2)
      ...)

  (MATCH foo
  | (*list [ (*symbol "foo") (*integer 1) (*integer 2) ])
      ...)


  # These two are equivalent
  (MATCH foo
  | &(foo ~a ~@b)
      ...)

  (MATCH foo
  | (*list [ (*symbol "foo") a @b ])
      ...)
As far as I can tell, this allows Nulan macros to do everything that Arc macros can do, even with a static type system.

-----

3 points by rocketnia 3338 days ago | link

Yeah! There's a certain way that it's really clear that macros and statically typed code can work together; just run the macros before interpreting the static types. Thanks for demonstrating that. I do think this can do everything Arc can do, like you say.

There are a couple of ways I think that can get more complicated:

In languages that resolve overloaded names in a type-directed way, I think the reconciling of static types and macros gets quite a bit more challenging due to the underlying challenge of reconciling name resolution with macros.

For instance, I think Idris has different meanings for the name (,) depending on if it occurs as a value of type (Type -> Type -> Type) or a value of type (a -> b -> (a, b)). In one case it's a constructor of tuple types (a, b), and in another case it's a constructor of tuple values (a, b).

Idris's macro system actually seems to have some special support for invoking the typechecker explicitly from macro code, which I think helps resolve names like those. I haven't actually succeeded in writing macros in Idris yet, and it seems to be a complicated system, so I'm not sure about what I'm saying.

Secondly, one potential goal of a macro system is that all the language's existing primitive syntaxes can turn out to be macros. That way, future versions of the language don't have to inherit all the crufty syntaxes of the versions before; they can stuff those into an optional library. If the language has sophisticated compile-time systems for type inference, term elaboration, name resolution, syntax highlighting, autocompletion, etc., then hopefully the macro system is expressive enough that the built-in syntaxes are indistinguishable from well-written macros.

Arc already gives us things like (macex ...) that can distinguish macros from built-in special forms, so maybe seamlessness isn't a priority in Arc. But if it were, and Arc had static types, we would probably want Arc to have type inference as well, which could complicate the macro system.

A lot depends on what Paul Graham expects from "true macros."

-----

3 points by Pauan 3338 days ago | link

I don't think name overloading is a problem, at least not in Nulan.

Macros deal with things at the syntax level, far earlier than any kind of type overloading.

As far as the macro is concerned, it simply sees the syntax

  (*list [ (*symbol ",") (*integer 1) (*integer 2) ])
It doesn't care what the type is, or the meaning, or anything like that. In some cases, the macro doesn't even know whether the symbol is bound or not!

Basically, I view type inference/checking/overloading occurring in a phase after macro expansion.

Is there any benefit to mixing the type overloading phase and the macro expansion phase?

----

It occurs to me that you might not be referring to a macro which expands to syntax, but instead using the syntax within the macro body itself.

In Nulan, that isn't a problem either. A macro is essentially a function from Code to Code, so the macro body is compiled/macro expanded/type checked just as if it were a function.

Thus any type overloading will happen inside the macro body, before the macro is ever run.

That does mean that the type checker must be able to overload based solely on the types found within the macro body. In other words, it cannot dynamically dispatch.

----

I don't think it's possible for the primitives to be macros, for the simple reason that the language needs axioms.

If you mean that the primitives should appear to be macros (even though they're implemented with compiler magic), then I agree with you.

-----

3 points by rocketnia 3337 days ago | link

"Macros deal with things at the syntax level, far earlier than any kind of type overloading."

If the macro itself is being referred to by an overloaded name, then the name needs to be resolved before we know which macro to call.

(I'm not sure if macros' names can be overloaded in Idris; I'm just guessing. Actually, I don't know if Idris lets users define overloaded names. I just know a few built-in names like (,) are overloaded.)

---

"If you mean that the primitives should appear to be macros (even though they're implemented with compiler magic), then I agree with you."

Yeah, that's what I mean. :)

-----

2 points by Pauan 3337 days ago | link

You're right, if you allow for users to overload multiple macros onto the same name based upon type, then it gets very messy.

But I think if your language allows for that, it's inconsistent with the way that macros work.

If you want powerful Arc-style macros, then the macro must be run before any types are known.

Any system which allows for that kind of type-based dispatch is different from a Lisp-style macro system, and is probably less powerful.

For example, you won't be able to create macros which create new variable bindings (like "let", "with", etc.) because the type of the variable is not known until after the macro is run.

I'm not sure what a system like that would look like, or if I would even call it a macro system.

I think it makes more sense to have two separate systems: a macro system that deals with syntax only, and an overloading system that deals with types. That's what Nulan does.

But perhaps there is room for a third system, somewhere in between the two: that third system could do type-directed syntax manipulation.

-----

2 points by Pauan 3334 days ago | link

I thought about this some more.

I have no experience with Idris or dependent types, so I may be completely wrong.

But from my understanding, a dependent type system allows for types to include values (which are evaluated at compile-time).

If so, then you might not even need macros at all to create the "," behavior.

Instead, you create a typeclass which will dispatch to the appropriate behavior:

  data Pair a b = pair a b

  interface Comma a b c where
    comma : a -> b -> c

  Comma Type Type Type where
    comma = Pair

  Comma a b (Pair a b) where
    comma = pair
Now whenever you use the "comma" function, it will dispatch depending on whether its arguments are Types or not:

  -- Type
  comma Integer Integer

  -- (Pair Integer Integer)
  comma 1 2
And since types may include arbitrary expressions, you can of course use the "comma" function inside of a type:

  foo : a -> b -> (comma a b)
Note: all of the above code is completely untested and probably wrong, but you get the idea.

Basically, your language needs first-class types, and the ability to run arbitrary expressions at compile-time (even within types), and then you can use an ordinary typeclass to get the desired type dispatch. No macros needed.

-----

2 points by rocketnia 3332 days ago | link

"If so, then you might not even need macros at all to create the "," behavior."

I never said the overloading of "," was accomplished with macros.

The example I gave was of a macro whose name was overloaded in a type-directed way, similarly to the way "," is overloaded in Idris. (Maybe the macro itself is named ",".) My point is that if the macro system is designed to support that kind of overloading, then sometimes a macro call will depend on intermediate results obtained by the typechecker, so we can't run the macroexpander in a phase of its own.

---

For the sake of remembering Idris more accurately, I checked out the Idris docs to look for the particular "macro system" I was dealing with.

It turns out what I mean by the "macro system" in Idris is its system of elaborator extensions (http://docs.idris-lang.org/en/latest/reference/elaborator-re...). The elaborator's purpose is to fill in holes in the program, like the holes that remain when someone calls a function without supplying its implicit arguments.[1]

It's pretty natural to handle name overloading in the same phase as implicit arguments, because it's effectively a special case. Thanks to dependent types and implicit arguments, instead of having two differently typed functions named (,), you could have one function that implicitly takes a boolean:

  (,) : {isType : Bool} ->
    if isType then (... -> Type) else (... -> (a, b))
  (,) = ...
The elaborator phase is complex enough that I don't understand how to use it yet, but I think almost all that complexity belongs to implicit arguments. Name overloading is probably seamless relative to that.

Another "macro system" I encountered in the docs was Idris's system of syntax extensions (http://docs.idris-lang.org/en/latest/tutorial/syntax.html). As far as I can tell, these apply before the elaborator and typechecker, but they might not be able to run arbitrary code or break hygiene. Maybe someday they'll gain those abilities and become a Nulan-like macro system.

[1] Implicit arguments effectively generalize type classes. Idris still has something like type class declarations, which it calls "interfaces," but I bet this is primarily so a programmer can define the interface type and the method lookup functions together in one fell swoop.

-----

5 points by Pauan 3693 days ago | link | parent | on: How do you see what a macro expands to?

First off, you're seeing the macro-expansion of the "mac" macro, not the macro-expansion of the "test" macro. Try this instead:

  arc> (mac test a `(list ,a))
  arc> (macex '(test a))
Secondly, you can also use "macex1" to only expand it one time:

  arc> (macex1 '(mac test a `(list ,a)))
Sometimes this produces nicer output.

-----

3 points by jsgrahamus 3693 days ago | link

Why does your example return (list (a)) instead of (list a)?

Steve

-----

3 points by akkartik 3693 days ago | link

Rest arg :)

-----

2 points by jsgrahamus 3692 days ago | link

???

-----

3 points by akkartik 3692 days ago | link

  (mac test a `(list ,a))
vs

  (mac test (a) `(list ,a))
Does that help?

-----

3 points by lark 3693 days ago | link

Thank you.

-----

3 points by Pauan 3698 days ago | link | parent | on: Two useful language extension ideas

You can indent a code block by 2 spaces to format it as code.

---

Your idea of types is similar to Predicate Dispatch[1]. I believe it's also similar to Racket's contracts[2].

This is an extremely powerful type system in the sense that you can express literally anything with it. However, I believe that a more restrictive type system can actually be better, because it provides more guarantees (especially at compile-time).

---

I don't think it's possible to pattern match on a function's arguments, body, and closure, simply because Racket doesn't expose those things.

I do approve of pattern matching, though.

---

* [1]: http://en.wikipedia.org/wiki/Predicate_dispatch

* [2]: http://docs.racket-lang.org/guide/contracts.html?q=contract

-----

2 points by Pauan 3719 days ago | link | parent | on: Why is "do" a macro and not a function?

That's a good point. In Nulan, you can't redefine things (all variables are constant). I'm not so worried about performance right now. My main concern is simplicity, since implementing "do" as a macro introduces a lot of extra complexity (because Nulan compiles to JavaScript).

-----

3 points by Pauan 3722 days ago | link | parent | on: Why is "do" a macro and not a function?

Well, no, because "do" returns the last argument, so it would be:

  (def do args (last args))

-----

2 points by Pauan 3759 days ago | link | parent | on: DrRacket(Libs) and Arc

I just fixed a bug in Arc/Nu, so now you can do this:

  (%:require racket/class)
  (%:require racket/gui/base)

  (= frame (%:new frame% (label "Example")))
Notice that you can use () rather than [], because in Racket the syntax reader converts [] into ().

You need to require "racket/class" because Arc/Nu only requires "racket/base", rather than all of "racket".

-----

2 points by ChristophRe 3759 days ago | link

in the end it would be nicer to have an Arc library that wraps the underlying gui racket library in the spirit of Arc. Arc is much simpler than scheme code. I does not like complicated syntax. I also does not like OO and classes of racket. It is the wrong way. Hashtable like in Javascript and writing your own OO-like library is much more flexible.

-----

1 point by akkartik 3759 days ago | link

You should build it :) Doesn't have to be complete at the start, just use it for a certain program and try to think of a cleaner way to express what you build. Then come ask us for help.

-----

2 points by ChristophRe 3759 days ago | link

Thank you very much. I assume that I could use another syntax for [...] like in Arc. But I did not find it. So often it is better to ask people you has better scheme background. I am not grown up with Lisp stuff.

-----

3 points by Pauan 3761 days ago | link | parent | on: DrRacket(Libs) and Arc

I would just like to point out that Arc/Nu is fully compatible with Arc 3.1, and it doesn't need conversions between Racket and Arc, so it's the easiest way to deal with Racket in Arc programs.

-----

2 points by Pauan 3791 days ago | link | parent | on: New logo

I personally think Arc is a fantastic way to learn Lisp:

#1 Arc is minimal. This makes it a lot easier to learn, because it has so few axioms.

#2 Arc heavily emphasizes recursion, and recursion is fundamental to computing in general. So it's important to understand recursion.

#3 You learn about closures and first-class functions, which are very useful and are used in lots of other languages (including JavaScript, Python, and Ruby).

#4 Arc's macros are simple enough that it's easy to fully understand how they work.

Arc is basically a stripped down minimal Scheme with Common Lisp unhygienic macros. It shouldn't be hard to convert the Scheme code in SICP into Arc.

As to whether Arc would be a good first programming language in general... I don't know. I suspect that if you take two programmers, one learns a functional language first, and the other learns an imperative language first, that they would have radically different mindsets.

Because of the recent surge of parallelism, functional languages have a significant advantage, because immutability and purity give both increased correctness and performance compared to mutability. So I don't think it's a bad idea to learn a more functional language like Arc first.

-----

2 points by jb 3791 days ago | link

Re #2 and #3: I think Clojure also puts a heavy emphasis on closures, first-class functions, and recursion (although it seems you have to jump through a hoop to get TCO working (Java and JavaScript don't support TCO (yet))).

Arc may be more minimal and have simpler macros, but I think it'll take me a lot less time to actually start making things with Clojure because there are more resources available for learning it, and it has good support for making web apps (good libraries, good compiler to JavaScript). The Arc docs seem to be good, but targeted more at people who are already familiar with Lisp.

I tend to learn best by creating things, and seeing as I already know HTML and CSS, a language that would allow me to dive right in by creating web apps would be great.

There's also the fact that if someone asked me why I chose Arc over any other Lisp, I wouldn't really know what to say. I like the sound of your philosophy, but at the moment I don't really have the knowledge to understand how Arc is different from other Lisps (other than by being very minimal).

Maybe once I've got some practical experience and a good grasp of the basic concepts behind Lisp and macros, I might start learning Arc, and be able to appreciate it for what it is.

-----

3 points by Pauan 3790 days ago | link

You're right, Clojure is not a bad choice either, in my opinion. It's even more heavily functional than Arc, so it's an excellent way to ease into functional programming. There are certain things I really dislike about Clojure, but on the other hand there are some things I absolutely love about it. I think it gets a lot of things right.

I think Arc is a better language for learning Lisp, but you are correct that Clojure would be better for writing actual applications that do things. Solving actual problems with actual applications can give you a lot of motivation, and motivation is important when learning anything.

-----

2 points by akkartik 3791 days ago | link

Sounds like you have a decision :)

The reason I use arc: I can't seem to stop. Before arc I tried to learn common lisp and scheme, but they required me to be persistent, and I kept falling off the wagon. With arc, I didn't need to be persistent. That was the concrete, tangible benefit of minimalism for me: the entire experience seemed to cater to me. There were fewer things that were like, "this may seem overly complex, but there's good reason for it, you'll understand when you gain experience." That argument is almost always a rationalization for accidental complexity (https://en.wikipedia.org/wiki/Accidental_complexity), in my experience.

Eventually I grew more fluent with common lisp and racket as well. You're right that they're more grown-up, and sometimes one needs something grown-up and industrial-strength. But arc was the gateway drug, and I can't seem to outgrow it.

I said this in another thread: "What really got me into arc was writing programs in one split window with the friggin compiler for my language in the other." (http://arclanguage.org/item?id=18954) That's really valuable, because to get really good at programming requires breaking out of every box other programmers create for you. Yes it's more immediately valuable to learn to use a few platforms or libraries. But understanding how they work, the design decisions and tradeoffs that are common to all platforms, that gives you superpowers in the long term.

In fairness, all my experiences predate clojure, which may well have all the same benefits. Several people have switched to it from arc in the past. I'd love to hear about your experiences after you try it out. I haven't much experience with it, though I've heard that the boxes it creates are harder to break out of (https://plus.google.com/110981030061712822816/posts/KaSKeg4v...)

I'm curious what docs you looked at that seemed to require familiarity with lisp. Feedback most appreciated.

-----

2 points by jb 3790 days ago | link

Hmm, some interesting reads.

These docs (https://arclanguage.github.io/ref/) are the ones I was talking about. I haven't read much, but there's no mention of parentheses anywhere. Also, there are symbols in circles to the left of concepts, e.g. F ! ? M, but no explanation of what they mean.

Generally, the docs seem rather terse to me; there's a lot of stuff that only makes sense now, after reading (http://aphyr.com/posts/301). "Clojure from the ground up" does start slowly, but by the end of Chapter 4, I really understood the idea of recursion and representing code as a tree, something that I don't think I could have got from the Arc docs.

-----

1 point by akkartik 3790 days ago | link

That's fair. I think we worked on the reference because the tutorial seemed pretty good. Do you think we need something in between?

Edit: I thought the tutorial had come up in this thread, but it hasn't. I'm not used to having two questions from newcomers at once :) Are you aware of http://old.ycombinator.com/arc/tut.txt?

-----

2 points by jb 3787 days ago | link

Ahh, I got a server error when I first tried to look at that; it seems to be working now, though. I've read the tutorial - it does seem pretty good. Thanks for the link :)

-----

1 point by akkartik 3787 days ago | link

Yeah, sorry about that. I think the HN guys made some changes and broke us for like a day.

-----


One big difference is that Arc is very minimal, while Common Lisp has lots of libraries and a large standard library. In Arc, you'll end up writing a lot of stuff yourself. But because Arc tends to be more concise than Common Lisp, this can be fun rather than frustrating.

In large part because Arc is so minimal, it only really has two compound data structures: cons cells and mutable key/value dictionaries.

With a bit of effort, it's possible to use Racket libraries and Racket data structures. It's easier to do this in Arc/Nu and Anarki, compared to Arc 3.1.

Arc also does not have a module system, so name collisions are a very real possibility. If your code base is small this isn't a problem, but it does make it a bit trickier to use libraries written by other people.

Another difference is that Arc takes after Scheme, so it has tail-call optimization and full first-class continuations. Recursion is quite common.

Like Scheme, Arc has a single namespace for both functions and variables, unlike Common Lisp which has separate namespaces.

Arc intentionally makes no promises of backwards-compatibility: new versions of Arc might be radically different and completely break your code. Common Lisp, on the other hand, tends to be very stable, and cares a lot about backwards-compat.

Essentially, you can think of Arc as being very similar to Scheme, but much more minimal and concise, and with Common Lisp style unhygienic macros.

-----

More