Arc Forumnew | comments | leaders | submitlogin
3 points by rocketnia 929 days ago | link | parent

I made an attempt at this once in Anarki, but I wasn't really satisfied with the result. The anarki package has a `#lang anarki` language, with a demo in the /lib/racket-lang-demo directory.

Living link: https://github.com/arclanguage/anarki/tree/master/lib/racket...

Pinned link: https://github.com/arclanguage/anarki/tree/a2569b1e071bd559b...

In particular, this is /lib/racket-lang-demo/lang-anarki-library.arc, which shows what it would look like to use Racket libraries and other Arc libraries from a `#lang anarki`-based library:

  #lang anarki
  (:provide anarki-library-export)
  
  (= racket-deep-dependency-export
    ($:dynamic-require "racket-deep-dependency.rkt"
      'racket-deep-dependency-export))
  ($:dynamic-require "lang-anarki-deep-dependency.rkt" #f)
  (load "plain-anarki-deep-dependency.arc")
  
  (= anarki-library-export
    (+ "from " racket-deep-dependency-export ", "
       lang-anarki-deep-dependency-export ", and "
       plain-anarki-deep-dependency-export "!"))

The `(:provide ...)` syntax is a special extension to the Arc language for the purposes of `#lang anarki`. Every `#lang anarki` file must start with `(:provide var1 var2 var3 ...)` fo r some number of variables (even zero). It describes what exports are visible to other Racket libraries.

I believe `#lang anarki` does something to make it so `(load "plain-anarki-deep-dependency.arc")` loads the file path relative to the directory the module is in.

There are a number of awkward things about using Arc to write libraries for Racket consumption:

- Racket languages typically have a notion of phase separation where some amount of the program (the compile-time part, i.e., phases 1 and up) happens during the process of compiling a module into a .zo file, and the rest of the behavior (the run-time part, i.e., phase 0) can be performed by loading the already-compiled .zo file. Arc programs are more like `#lang racket/load` in that they only begin macroexpanding the later expressions in the file after they've run the run-time parts of the previous expressions, so it's like the whole macroexpansion process has to wait until run time. The only things `#lang anarki` does at compile time are reading the file's s-expressions and processing the `(:provide ...)` directive.

- Racket and Arc both have macro systems. They both involve writing macro definitions in source-to-source styles that usually make it easy to write a macro that abstracts over another macro call. Sometimes, in more advanced cases, these styles require more attention to get the hygiene right. However, they use different "source" types (Racket's syntax objects vs Arc's plain s-expressions), and they have rather different approaches to hygiene (Racket's lexical information carrying module imports and sets of scopes, vs Arc's gensyms and global variable redefinition warnings). This means mixing Racket macros with Arc macros is one of the advanced cases; I expect that to get the interactions right, it may be necessary to do some explicit conversions between "source" types, as well as being proficient in both languages' approaches to hygiene.

- As far as a module system goes, Arc traditionally has no way to allow multiple pieces of code to see different bindings of the same top-level variable name. (Framewarc and I believe Arc/Nu have approaches to this, but they involve writing all macros differently than before.) Hence, two Arc libraries might fail to be usable together due to mutual clobbering of the same variable, which isn't a typical situation with Racket modules. If and when modular techniques catch on for Arc, those techniques may still be challenging to reconcile with Racket's techniques, so a `#lang anarki`-based library may still not present a very familiar interface to other Racket ecosystem programmers.

- Arc is a lisp-N with its various namespaces, like `setforms`, `defcall`, and `coerce`, stored as entries in global hash tables. Racket is a lisp-N with its various namespaces, like `set!` transformers and `match` expanders, stored on the same compile-time object by implementing multiple interfaces (structure type properties/generic interfaces). This is one particular place where reconciling the modularity approaches may be difficult.

- Racket's compiler tooling tries to determine statically what files a file depends on, both so that `raco make` and `raco setup` can recompile the file if its dependencies have changed and so that if a file is bundled up with `raco distribute` or `raco exe`, its dependencies are included in the bundle. This is something I didn't tackle at all with `#lang anarki`. Things like `load` and `dynamic-require` make it difficult to keep track of dependencies statically this way.

The design I chose for `#lang anarki` was basically to get something tolerable working with as little effort as possible. There's probably a lot of room for improvement, so I didn't consider it stable enough to show off in the `anarki` Racket package documentation.

I think a somewhat better approach would involve:

- Giving Anarki a read-time namespace system like Common Lisp's, to solve Arc's inadvertent name clobbering issues (while potentially still permitting intentional clobbering).

- Going ahead and macroexpanding a whole `#lang anarki` file at compile time, evaluating nothing but the `mac` definitions at first, even though that's not the usual Arc `load` semantics. User-defined macros can sometimes be slow, making it worthwhile to expand them before producing the .zo. Only so much Arc code actually cares about running certain expressions before others are expanded, and those cases can use a CL-style `eval-when` approach.

- Assembling static information about the module just by using `eval-when` compile-time mutable definitions. Then we could dispense with `(:provide ...)` and just have a global mutable table that collects all the exports of the current module.



3 points by rpmuller 920 days ago | link

Thanks for such a thorough answer. Lots to learn.

-----