1. 21
  1. 7

    To me, this has been a pretty convincing argument to sexp.

    The Rhombus codes were often shorter, yes, but I found the Racket versions of the code easier to follow, and the macros noticeably cleaner. The Rhombus macros have way too much stuff encoded in strings - no, thanks.

    1. 7

      Those aren’t strings, they’re quoted forms. Double-quote characters delimit strings.

      1. 3

        ‘$exp |> $f $rest …’

        That sure does look like a string to me: starts with a ’, ends with an ‘, with code between that has different syntax than the rest of Rhombus. It may not be technically a string, but it’s encoding a different syntax within a block of text than the rest of the language.

        In contrast, in Racket, quoted or unquoted, it’s the same syntax.

        1. 3

          I don’t know what to say. It’s not a character string, it’s a quoted form. The syntax is different because it’s defining new syntax.

          1. 2

            This syntax looks like a quasiquote to me, and for this case, Racket syntax is not an improvement.

             `(,f ,@rest)
            

            vs

             '$f $rest ...'
            
            1. 2

              It looks similar, but isn’t.

              Quasiquote follows the same syntax as the rest of the language, it doesn’t need to be wrapped between apostrophes. There’s the backtick, which is just syntax sugar for (quasiquote). The ‘’ wrapping in Rhombus do not appear to be syntax sugar, nor does the $ sign. In Racket, you quote the form, in Rhombus, you wrap it between apostrophes, because the Rhombus syntax doesn’t make it clear what exactly is the form.

        2. 1

          The Rhombus macros have way too much stuff encoded in strings - no, thanks.

          That stood out to me as well. Seems like a big whiff to move all of the metaprogramming into strings.

          1. 5

            As @snej pointed out above, that is not a string, but a syntax template. We initially used '(...) as a way to indicate syntax template, but then there was a proposal to change that to '...', so we experimented with it and so far it seems to work OK.

            See the proposal at https://github.com/racket/rhombus-prototype/discussions/213.

            1. 2

              Thanks for that link, the discussion helps ease my gut reaction a lot. I suspect with proper syntax highlighting all worries would go away.

              It’s interesting that one of the pros for using single quote instead of backtick is markdown. Is there an intention that “literate” programming will be easy in Rhombus or is the consideration based on some other markdown-related concern?

        3. 6

          As a longtime Lisp-averse person, I find this appealing. I’ve seen things like Wisp that are more-readable syntactic sugar around S-exprs, but they still preserve the underlying Polish-notation (prefix operator) nature that I find only marginally clearer than Forth.

          Shrubbery seems like a good middle layer, a representation that’s still homoiconic but richer and more flexible. The Shrubbery spec itself is difficult to follow because it’s necessarily rather abstract, but in conjunction with Rhombus syntax it makes more sense.

          It’s also interesting to think that one could build other languages atop Shrubbery. If I designed one I would of course call it “Ni!”

          1. 5

            A rather interesting observation to make here is that while I could have implemented a similar threading operator in Racket, I opted not to, because I felt that the extra syntactic overhead required to write the macro would outweigh its benefits, while in Rhombus, the lighter syntax actually made me more inclined to use macros3.

            Funny he mentions that cuz over in Clojure, we have a much lighter macro system that trades raw power for ease and everyday usage. (Turns out, folks do write some macros but still mainly stay in functions as they compose better.)

            To give an example, in Clojure, we have the thread-first (->) and thread-last (->>) macros (examples from ClojureDocs):

            (-> person :employer :address :city)
            ;; is equivalent to
            (:city (:address (:employer person)))
            
            (->> (range)
                 (map #(* % %))
                 (filter even?)
                 (take 10)
                 (reduce +))
            ;; is equivalent to
            (reduce +
                    (take 10
                          (filter even?
                                  (map #(* % %)
                                       (range)))))
            

            The implementations are pretty simple as seen in clojure.core. They take the forms, apply transformations, and return the quasi-quoted result.

            Compare this with Racket’s threading library implementation. Maybe it’s my relative inexperience with Racket’s syntax-case, but I feel like the difference is vast.

            1. 7

              Compare this with Racket’s threading library implementation. Maybe it’s my relative inexperience with Racket’s syntax-case, but I feel like the difference is vast.

              To understand what’s going on here you have to see that hygenic macros were specifically designed to avoid a specific problem with CL’s macro system: that of accidental symbol capture. Scheme’s macros go to great lengths to make symbol capture completely impossible, at the cost of giving up the conceptual simplicity of defmacro.

              Clojure came along and solved the same problem with a much easier solution: ban backticked symbols from being used as identifiers unless they used auto-gensym. It’s still basically impossible to accidentally do symbol capture. You have to go out of your way to do it, and it’s really obvious when you do. (Almost always still a mistake, but at least you won’t get bitten by it unaware!) But the same bug was solved with a tiny fraction of the conceptual cost.

              (This has nothing to do with Rhombus FWIW; just a general comparison between Schemes vs Clojure and other newer lisps which have learned from Clojure.)

              1. 5

                The differences here aren’t related to hygiene at all. You could write threading exactly like that in Racket. The different implementation is (a) abstracting things out to use in a bunch of variants of the threading macro and (b) specifying error handling to give better error messages.

                1. 6

                  Here’s the code for -> written in Racket exactly like in Clojure. The differences from the original are mostly because of syntax objects vs lists, which is about hygiene (I disagree with your characterization of that situation in several ways, but that’s not really relevant here):

                  #lang racket
                  
                  (require (for-syntax syntax/stx))
                  
                  (define-syntax (-> stx)
                    "Threads the expr through the forms. Inserts x as the
                    second item in the first form, making a list of it if it is not a
                    list already. If there are more forms, inserts the first form as the
                    second item in second form, etc."
                    (syntax-case stx ()
                      [[-> x . forms]
                       (let recur [[x #'x] [forms #'forms]]
                             (if (stx-pair? forms)
                                 (let* [(form (stx-car forms))
                                        (threaded (if (stx-pair? form)
                                                      #`(#,(stx-car form) #,x #,@(stx-cdr form))
                                                      #`(#,form #,x)))]
                                   (recur threaded (stx-cdr forms)))
                                 x))]))
                  
                  (-> 3 (+ 1) -) ;; produces -4
                  

                  Here’s how I’d write the macro in Racket, going for simplicity:

                  (define-syntax (-> stx)
                    (syntax-parse stx
                      [(-> x) #'x]
                      [(-> x (f args ...) . rest) #'(-> (f x args ...) . rest)]
                      [(-> x e . rest) #'(-> (e x) . rest)]))
                  
                2. 3

                  Thanks for the details! I knew it had something to do with hygiene but I don’t fully understand the differences so this makes a lot more sense.

                  (This has nothing to do with Rhombus FWIW; just a general comparison between Schemes vs Clojure and other newer lisps which have learned from Clojure.)

                  Yeah, I didn’t mean it to be about Rhombus necessarily, just interesting to see how high a barrier Racket’s macro system is in practice.

                3. 4

                  Well, lexi-lambda has a black belt in Racket macros ;-)

                  The code defines several macros and lexi-lambda made sure to share relevant parts of the implementation. The use of syntax-classes mean errorneous applications of the macros will give user friendly error messages.

                  For a one-to-one comparison you need a Racket implementation that only implements ->. And you need to compare both the quality of error messages, when the macros is used incorrectly - and check whether the error locations are reported correctly.

                4. 4

                  my main pain point while writing Rhombus was an increased difficulty in doing REPL-based development.

                  In particular, in other Lisps, I often find it easy to quickly prototype ideas by iteratively pasting sub-expressions (delineated by parentheses) into the REPL to analyze and debug what any given piece of code is doing. […]

                  However, looking forwards to the future of Rhombus, given the great emphasis many Lisp developers place on the virtues of their REPL-based workflows, this likely a problem that may need to be addressed if Rhombus is hoping to make inroads on any existing lisp communities.

                  This isn’t just a problem you can address with more effort; this is a shortcoming inherent in the approach they’ve chosen. Especially if the Rhombus core devs admit to themselves not being repl users, I find it extremely unlikely that this will improve, if indeed it’s even possible. Everyone knew what they were giving up going into it; this shouldn’t come as a surprise.

                  1. 4

                    I have a feeling /u/technomancy might know already what Flatt meant, but I feel it is a good thing to clear things up for others:

                    Rhombus will support a repl for experimenting with expressions, statements and running tests etc. In fact he capabilities of the terminal repl has been improved considerably lately in order to support Rhombus. What “REPL-based development” in the quote refers is “modifying a running image in the repl and then dump the image”. The problem with that approach is accidently relying on state in the image without realizing it.

                    See more here: https://blog.racket-lang.org/2009/03/the-drscheme-repl-isnt-the-one-in-emacs.html

                    For the opposite view: https://stackoverflow.com/questions/480083/what-is-a-lisp-image

                    1. 3

                      modifying a running image in the repl and then dump the image

                      Hm; while that might be what CLers mean by the term, I don’t see any reason to think the author of this article was thinking of image-dumping as a requirement for “repl-based development”. Users of most lisps just use it to mean taking advantage of continual interaction with the running process during development, sending over a few expressions at a time, and avoiding restarting the process unnecessarily.

                      The article never mentions images but it does call out the lack of structure as the specific cause of the difficulty caused by Rhombus, and it says the author restarts the process repeatedly as a workaround.

                      1. 3

                        You are right, the article doesn’t mention image dumps. I strayed away from the point: Rhombus will support a repl.

                        The article is unclear what the “repl-based development” means in context. Having followed Rhombus from the side-line I just wanted to make clear, that a repl will be available.

                  2. 4

                    I find once you get used to reading s-exps, they’re actually much easier to scan than pretty much any other syntax I’ve seen because they have very few rules and gotchas. The more syntax you have the more mental overhead you end up introducing, and the more potential for errors where the code isn’t doing quite what it looks like it’s doing.

                    Other advantages include having structural editing where you can manipulate your code as a tree and tell the editor to do semantic operations like moving nodes around, and being able to easily transform code as data using macros.

                    Vast majority of people who’ve used s-exps for even a few weeks realize that the syntax is just fine, and I suspect this is why ideas like Rhombus never end up catching on.

                    1. 3

                      Agree. If you want a modern lisp, Racket is a compelling choice.

                      Sadly the vast minority of programmers don’t want a lisp - I know , I know - they don’t know what they’re missing.

                      I’m hoping they’ll get hooked on Rhombus, and come over to Racket.😁

                      1. 3

                        If Rhombus lowers the barrier and gets more people using Racket, I think that provides value all of its own. Even if it ends up being training wheel people can use while they’re getting comfortable with the language, that’s a good outcome.