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

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 ^_^

reply

1 point by shawn 2 hours 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))
  1
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.

reply

1 point by akkartik 8 hours ago | link | parent | on: Ask AF: Ordered Tables?

How about AutobiographicalDict? ^_^

More serious suggestion: Journaling Dict. Maybe the Arc type could be `jtable`?

'order' is pretty ambiguous here. The fact that some places use the word to mean what we mean doesn't seem like sufficient reason to follow suit.

reply

1 point by kinnard 7 hours ago | link

I mean 'insertion order'

reply

1 point by akkartik 5 hours ago | link

But do you see that other interpretations are possible?

reply

1 point by kinnard 5 hours ago | link

Yes like a variety of consistent sort orders

reply

1 point by akkartik 5 hours ago | link

Not quite. The distinction I'm drawing is between ordering the elements based on their intrinsic properties, and ordering the elements based merely on the order they're inserted in.

reply


Yeah, memory usage was a big reason I stopped trying to maintain servers in Arc (and went down my insane yak-shaving rabbithole of trying to reinvent the entire computing stack from scratch).

reply

1 point by akkartik 13 hours ago | link | parent | on: Ask AF: Advantages of alists?

I'm having a lot of trouble parsing this comment.

> I recommend not expecting `quote` or `quasiquote` to be very useful outside the context of metaprogramming.

My immediate reaction is to disagree. A lot of the reason Lisp is so great is that quasiquotation is orthogonal to macros/metaprogramming.

    > ; Should this cause an error, or should it result in the same thing as
    > ; '(let i 0 `{,++.i "foo"}) or '(let i 0 `{,++.i "foo"})?
Those two fragments are the same?

In general it feels unnecessarily confusing to include long doc comments in code fragments here. We're already using prose to describe the code before and after.

Code comments make sense when sharing a utility that you expect readers to copy/paste directly into a file to keep around on their disks. But I don't think that's what you intend here?

Finally, both your examples seem to be more about side effects in literals? That is a bad idea whether it's a table literal or not, and whether it uses quasiquoting or not. Do you have a different example to show the issue without relying on side-effects?

reply

2 points by akkartik 1 day ago | link | parent | on: Ask AF: Ordered Tables?

That Wikipedia page seems to only talk about C++. I just checked and C++'s `map` doesn't support indexing by integer: http://www.cplusplus.com/reference/map/map.

So I don't think this is something anybody supports:

  arc> (= grail (otable a "up" b "down"))
  arc> grail.0
  "up"
Ordered associative containers merely have a well-defined order for iterating over. And the order has nothing to do with the order in which elements were inserted.

So what you're asking for is interesting and plausible, but I don't think "ordered tables" is quite the precise label for it.

reply

2 points by kinnard 1 day ago | link

Damn. I thought I might've gotten that mixed up. Should've checked.

I think clojure's array-maps do it but with some limitations:

https://clojure.org/reference/data_structures#ArrayMaps

http://arclanguage.org/item?id=21014

reply


> '("a" "list" "of" "elements")/sym => (map [coerce _ 'sym] '( "a" "list" "of" "elements"))

That's starting to get outside the look and feel of a Lisp. Usually you look to the start of any form to see what it is. The fact that quoted lists can get a post-processor feels more like Forth.

And the `/sym` somehow now has to automatically do different things for a list or other expression? That feels more like APL than Lisp.

reply


I'm skeptical. Are you doing coercions that often? That seems like bad practice. But if you share a little program or app, that may persuade me otherwise.

For coercions, I usually use the `as` alias:

    (as type expr)
rather than

    (coerce expr 'type)
I find it a lot more convenient because `type` is usually atomic while `expr` can be arbitrarily complex. It makes for a more readable result for the short arguments to come first.

In Anarki, `as` is more common than `coerce`, though we still do a lot of `coerce` that could probably stand to be cleaned up:

    $ grep '[:(]as\>' lib/*.arc |wc -l
    26
    $ grep '[:(]coerce\>' lib/*.arc |wc -l
    15

reply

2 points by kinnard 1 day ago | link

Touché.

reply


From http://www.arclanguage.org/tut.txt:

"There's one thing you can't do with functions that you can do with data types like symbols and strings: you can't print them out in a way that could be read back in. The reason is that the function could be a closure; displaying closures is a tricky problem."

I've sometimes wondered just what the connotation of 'tricky' is here. Is it hard in some theoretic sense, or just "Arc is a prototype so we don't have this yet"?

reply

3 points by rocketnia 4 days ago | link

(Edit: Oops, I replied out of order and didn't read shawn's comment with the elisp examples before writing this.)

I suspect what pg means by it is primarily that it's tricky to do in Racket (though I'm not sure if it'd be because there are too few options or too many).

Essentially, I think it's easy to display a closure by displaying its source code and all the captured values of its source code's free variables. (Note that this might be cyclic since functions are often recursive.)

But there is something tricky about it, which is: What language is the source code in? In my opinion, a language design is at its most customizable when even its built-in syntaxes are indistinguishable from user-defined ones. So the ideal displayed format of a function would ideally involve some particular set of user-definable syntaxes. Since a language designer can't anticipate what user-defined syntaxes will exist, clearly this decision should ultimately be up to a user. But what mechanism does a user use to express this decision?

As a baseline, there's at least one straightforward choice the user can make: The one that expresses the code in terms of Arc special forms (`fn`, `assign`, `if`, `quote`, etc.) and Arc functions that aren't implemented in Arc (`car`, `apply`, etc.). In a language that isn't aiming for flawless customizability, this is enough.

Now suppose we try to generalize this so a user can choose any set of syntaxes to express things in terms of -- say, "I want the usual language, but with `=` treated as an additional built-in." If the code contains `(= x (rfn ...))`, then the macroexpander at some point needs to expand the `rfn` without expanding the `=`. That's not really viable in terms of Arc's macroexpander since we don't even know `(rfn ...)` is an expression in this context until we process `=`. So this isn't quite the right generalization; the right generalization is something trickier.

I suppose we can have every function printed in terms of its pre-macroexpansion source code along with printing all the macros and other macroexpansion-time state it happened to rely on as it macroexpanded the first time. That would narrow down the problem to how to print the built-in functions. And we could solve that problem in the language design by making it so nothing ever captures a built-in function as a first-class value, only as a late-bound reference to the global variable it's accessible from.

Or we could have the user specify their own macroexpander and make it so that whenever a function is printed, if the current macroexpander hasn't expanded that function yet, it does so now (just to determine how the function is printed, not how it behaves). This would let the user specify, for instance, that `assign` expands into `=` and `=` expands into itself, rather than the other way around.

These ideas are incomplete, and I think making them complete would be pretty tricky.

In Cene, I have a different take on this: If a function is printable (and not all are), then it's printable because it's a callable struct with a tag the printer knows about. It would be printed as a struct. The function implementation wouldn't be printed. (The user could look up the source code information based on the struct tag, but that's usually not printable.) There may be some exceptions at the REPL where information is printed that usually isn't available, because the REPL is essentially a debugging context and the debugger sees all. (Racket's struct inspectors express a similar debugger-sees-all principle, but I haven't seen the REPL take advantage of it.)

reply

2 points by shawn 3 days ago | link

You're hitting on a problem I've been thinking about for years. There are a few reasons this is tricky, notably related to detecting whether something is a variable reference or a variable declaration.

  (%language arc
    (let in (instring "  foo")
      (%language scm
        (let-values (((a b c) (port-next-location in)))
          (%language el
            (with-current-buffer (generate-new-buffer "bar")
              (insert (prin1-to-string c))
              (current-buffer)))))))

To handle this example, you'll need to know whether each form is a function call, a variable definition, a list of definitions (let-values), a function call, and which target the function is being called for.

For example, an arc function call needs to expand into `(ar-apply foo ...)`

And due to syntax, you can't just handle all the cases by writing some hypothetical very-smart `ar-apply` function. If your arc compiler targets elisp, it's tempting to try something like this:

  (ar-apply let (ar-apply (ar-apply a (list 1))) ...
which can collapse nicely back down to

  (let ((a 1)) ...)
in other words, it's tempting to try to defer the "syntax concern" until after you've walked the code and expanded all the macros. Then you'd collapse the resulting expressions back down to the language-specific syntax.

But it quickly becomes apparent that this is a bad idea.

Another alternative is to have a "standard language" which all the nested languages must transpile tO:

  (%let in (%call instring "  foo")
    (%let (a b c) (%call port-next-location in)
      (|with-current-buffer| (%call generate-new-buffer "bar")
        (%call insert (prin1-to-string c)
          (%call current-buffer)))))
Now, that seems much better! You can take those expressions and easily spit out code for scheme, elisp, arc, or any other target. And from there it's just a matter of adding shims on each runtime.

The tricky case to note in the above example is with-current-buffer. It's an elisp macro, meaning it has to end up in functional position like (with-current-buffer ...) rather than something like (funcall #'with-current-buffer ...)

There are two ways to deal with this case. One is to hook into elisp's macroexpand function and expand the macros as you go. Emacs calls this eager macroexpansion, and there are some cases related to autoloading (I think?) that make this not necessarily a good idea.

The other way is to punt, and have the user indicate "this is an elisp form; do not mess with it."

The idea is that if the symbol in functional position is surrounded by pipe chars, then the compiler should leave its position alone but compile the arguments. So

   (|with-current-buffer| foo
     (prn (|buffer-string|)))
That works quite nicely, untll you try this:

  (|let| ((|a| 1) (|b| 2))
    (+ |a| |b|))
Then you'll be in for a nasty surprise: not only does it look visually awful and annoying to write, but it won't work at all, because it'll compile to something like this:

  (let (ar-funcall2 (a 1) (b 2))
    (ar-funcall2 _+ a b))

I am not sure it's possible to escape the "syntax concern". Emacs itself had to deal with it for user macros. And the solution is unfortunately to specify the syntax of every form explicitly:

https://www.gnu.org/software/emacs/manual/html_node/elisp/In...

  (def-edebug-spec let
       ((&rest
         &or symbolp (gate symbolp &optional form))
        body))
Ugh, augh, grawr. You can see how bad it would be to curse the user with having to do this for every macro they write.

Yet I am not sure it's possible to escape this fate. And it seems to work well in emacs.

Hopefully some of that might be helpful on your quest. The goal is worth pursuing.

reply

3 points by rocketnia 3 days ago | link

I can't speak to elisp, but the way macro systems work in Arc and Racket, the code inside a macro call could mean something completely different depending on the macro. Some macros could quote it, compile it in their own way, etc. So any code occurring in a macro call generally can't be transformed without changing the meaning of the program. Trying to detect and process other macro calls inside there is unreliable.

I have ideas in mind for how macro systems can express "Okay, this macro call is over; everything beyond this point in the s-expression is an expression." But that doesn't help with Arc or Racket, whose macro systems aren't designed for that.

So something like your situation, where you need to walk the code before knowing which macroexpander to subject each part of it to, can't reliably treat the code as code. It's better to treat the code as a meaningless soup of symbols and parentheses (or even as a string). You can walk through the data and find things like `(%language ...)` and treat those as escape sequences.

(What elisp is doing there looks like custom escape sequences, which I think is ultimately a more concise way of doing things if new macro definitions are rare. It gets into a middle ground between having s-expression soup and having a macro system that's designed for letting code be walked like this.)

Processing the scope of variables is a little difficult, so my escape sequences would be a bit more verbose than your example. It's not like we can't take a Racket expression and infer its free variables, but we can only do that if we're ready to call the Racket macroexpander, which isn't part of the approach I'm describing.

(I heard elisp is lexically scoped these days. Is that right?)

This is how I'd modify the escape sequences. This way it's clear what variables are passing between languages:

  (%language arc ()
    (let in (instring "  foo")
      (%language scm ((in in))
        (let-values (((a b c) (port-next-location in)))
          (%language el ((c c))
            (with-current-buffer (generate-new-buffer "bar")
              (insert (prin1-to-string c))
              (current-buffer)))))))
Actually, instead of just (in in), I might also specify a named strategy for how to convert that value from an Arc value to a Racket value.

Anyhow, once we walk over this and process the expressions, we can wind up with generated code like this:

  ; block 1, Arc code
  (fn (__block2 __block3)
    (let in (instring "  foo")
      (__block2 __block3 in)))
  
  ; block 2, Scheme code
  (lambda (__block3 in)
    (let-values (((a b c) (port-next-location in)))
      (__block3 c)))
  
  ; block 3, elisp code
  (lambda (c)
    (with-current-buffer (generate-new-buffer "bar")
      (insert (prin1-to-string c))
      (current-buffer)))
We also collect enough metadata in the process that we can write harnesses to call these blocks at the right times with the right values.

This is a general-purpose technique that should help with any combination of languages. It doesn't matter if they run in the same address space or anything; that kind of detail only changes what options you have for value marshalling strategies.

I think there's a somewhat more convenient approach that might be possible between Arc and Racket, since their macroexpanders both run in the same process and can trade off with each other: We can have an Arc macro that expands its body as Racket code (essentially Anarki's `$`) and a Racket macro that expands its body as Arc code. But there are some difficulties designing the latter, particularly in terms of Racket's approach to hygiene and its local macros, two things the Arc macroexpander has zero concept of. When we switch from Racket to Arc and back to Racket, the hygiene information and local macro scopes will probably be obliterated.

In your arcmacs project, I guess you might also be able to have an Arc macro that expands its body as elisp code, an elisp macro that expands its body as Racket code, etc. :-p So maybe that's the approach you really want to take with `%language` and I'm off on a tangent with this "escape sequence" interpretation.

reply

2 points by akkartik 4 days ago | link | parent | on: Creating Languages in Racket

That's really cool, thanks for making the case for Lumen. I now have it on my todo list to determine how much the standard library of Lumen matches the names Arc uses.

On a slight tangent, ycombinator.lol is not affiliated with Y Combinator, and https://github.com/lumen-language is not affiliated with Scott Bell the creator of Lumen. I clarify this because it took me a while to figure out. Am I characterizing it right, Shawn?

reply

2 points by i4cu 2 days ago | link

> I now have it on my todo list to determine how much the standard library of Lumen matches the names Arc uses.

Lumen looks awesome. I was looking for the docs to determine how much of arc actually exists within lumen, but I couldn't find anything. So if you do this, please let me know.

Also, If I can get some time down the road, I'd like to implement some basic dom manipulation functions. Personally I see Lumen as the best means to do mobile app development in Arc (which is probably one of the best things that can happen for Arc IMHO). Arc on the server side, Lumen on the client side and a code base that's useable by both would be really nice.

reply

2 points by shawn 4 days ago | link

Correct! https://github.com/lumen-language is to lumen as to https://github.com/arclanguage is to arc.

ycombinator.lol is just because fixed-point combinators are funny:

  (define Y (λ(b)((λ(f)(b(λ(x)((f f) x))))
                  (λ(f)(b(λ(x)((f f) x)))))))
It's amusing that it's even possible, and it looks visually like what happens when you accidentally `cat` a binary file to your terminal.

EDIT: I typo'd the link; The magic trick is at https://news.ycombinator.com/item?id=17963471.

reply

2 points by akkartik 4 days ago | link

> https://github.com/lumen-language is to lumen as to https://github.com/arclanguage is to arc.

Touche :) It would be a little clearer if the contributors for the organization were public.

reply

3 points by akkartik 9 days ago | link | parent | on: Ask AF: Advantages of alists?

Just to recap my opinion that I gave you over chat: the advantage alists have is that they're simple to support. Along pretty much any other axis, particularly if they grew too long, you'd be better off using some other data structure. Which one would depend on the situation.

reply

More