Lisp's Influence on Ruby

Posted by tacoda 5 days ago

Counter256Comment86OpenOriginal

Comments

Comment by jksmith 2 days ago

Now that I'm out of the corporate tyranny and have my own company, I use lisp for everything. There's certain satisfaction in writing config files and persisting data directly in s-expressions. Any json requirements are triggered by exports to foreign systems.

Comment by Blikkentrekker 2 days ago

That JSON prohibits trailing commata makes it an absolute pain to work with in practice.

I also like how in Haskell:

   something =
     { element
     , element1
     , element2
     , element3
     }
Is an actually idiomatic way to deal with the lack of trailing commata.

Comment by kazinator 2 days ago

I did something like that in C++ circa 1998, before seeing it anywhere else:

  MyClass::MyClass(foo bar, int arg1, int arg2)
  : Base(bar)
  , member1(arg1)
  , member2(arg2)
  {
  }

Comment by shawn_w 2 days ago

You see that style in SQL too.

Comment by NuclearPM 1 day ago

Commata?

Comment by Ferret7446 2 days ago

Not really? A linter/formatter takes care of it.

Comment by chirsz 2 days ago

Comment by atcol 2 days ago

Which Lisp, out of interest?

Comment by iLemming 2 days ago

Does it really matter? There's a point in every Lisper's life, a threshold after which the question becomes immaterial - you'd stop thinking about intricacies of whatever Lisp and focus on the platform specifics instead. Any given day I might program in three-four different Lisp dialects, e.g. Clojure/Clourescript, Fennel, Elisp, Janet, etc. and it practically feels like I'm using the same PL. While switching between TS and JS (same family) never feels even close - there's always some mental burden.

Comment by andsoitis 2 days ago

> Does it really matter?

not philosophically, but certainly practically. To double down, if all lisps are roughly equivalent from a language POV, then you'd want to pick the one or two that will give you the most practical advantage (libraries, documentation, dev environment, etc.)

Comment by jasaldivara 2 days ago

I don't want to be a gatekeeper, but Clojure, Janet and similars doesn't even have cons cells; that's hardly 'the same programming language'.

Comment by tacoda 2 days ago

I would call these different dialects of Lisp. The data doesn’t have to be a function. It’s illustrative. The patterns of application still work. What’s the difference if delimiters are different or if you are calling JVM libraries? The high-level ideas are still right there. Consider JavaScript. It is definitely not a Lisp, but if you model it as Lisp in C’s clothes, then all of a sudden IIFEs make total sense. The point is that it’s a helpful mental model for languages other than Lisp.

Comment by arunix 2 days ago

Is the lack of cons cells a significant limitation?

Comment by ux266478 1 day ago

Limitation is the wrong way to think about things when computational equivalence is in play. It's about mental foundation. Lisp at its core is like driving a Turing machine, Clojure is not.

Comment by iLemming 1 day ago

[dead]

Comment by chabska 2 days ago

If the difference didn't matter, we wouldn't have so many different lisps. Obviously the difference mattered enough to the people that created Common Lisp when Scheme already existed. Rich Hickey thought it mattered when he created a completely new Lisp instead of just porting Scheme to the JVM.

Comment by veqq 2 days ago

> If the difference didn't matter, we wouldn't have so many different lisps

Literally the opposite. We can make and use so many, because writing them is more or less the same. We can quickly throw together a new lisp for a new platform or such and use it without problem.

Comment by allthetime 2 days ago

Why is it necessary to throw together a new lisp and not just use an existing one?

Comment by tacoda 2 days ago

Technically when you write in the domain, you are effectively making your own Lisp and then using it. It’s one of the amazing things that macros can do.

Comment by arikrahman 2 days ago

Even the Lisps have Lisps. Like Clojure with ClojureScript, CLR, ClojureDart, Jank... etc.

Comment by slifin 2 days ago

Yes though they're trying to be effectively the same lisp

I do love that I learnt Clojure once like 5-7 years ago and more and more dialects keep expanding the choice of runtime I can target

Comment by arikrahman 14 hours ago

It's the mustard seed that keeps growing! And without the fragmentation other PL's suffer from, which are reminiscent of the Tower of Babel, conversely.

Comment by jksmith 2 days ago

I use the Franz Allegro toolchain exclusively.

Comment by hyperrail 2 days ago

One way I find traditional Lisp style more painful for functional code than Ruby is that fully functional-style Lisp pushes me to read and write code the opposite way from how I think about it. In the author's example:

    orders
      .select { |o| o.placed_at > 1.week.ago }
      .group_by(&:customer_id)
      .transform_values { |group| group.sum(&:total) }
the equivalent Lisp code would either be written in imperative style as multiple statements that each write to a temporary variable or (let) binding, or would look like this:

    (reduce #'+
      (map (lambda (o) (getf o 'total))
        ; this group_by replacement function
        ; might be written as hash-table code
        (my-group-by 'customer-id
          (remove-if-not
            (lambda (o)
              (>
                (getf o 'placed-at)
                (- (my-now) (* 60 60 24 7))))
            orders))))
where I now have to read from bottom to top to understand the order of operations on the `orders` record set, even though when I wrote the code earlier, I "logically" thought from first operation to last when deciding which high-level operations to use in which order.

Other imperative languages that support functional code either make you do things imperatively to get the "logical" ordering of functional operations like I feel Lisp pushes you to do, or they do something like Ruby where things can be chained left to right in a "single" statement even for operations that were not thought of ahead of time by the creators of opaque data structures you later need to operate on. (Everything is a user-extensible object like Ruby, unified function call syntax in D, extension methods in C#, or pipelines of structured objects in PowerShell.)

Comment by tmtvl 2 days ago

It could just be written like:

  (~> orders
    (filter (lambda (order)
              (timestamp> (order-date order)
                          (timestamp- (now) 7 :days))))
    (group-by #'order-customer-id)
    (mapcar (lambda (group)
              (reduce #'+ group :key #'order-total)))
But I prefer the typical Lisp code where I get the sums of the totals of the orders with the same customer ID which were placed in the past week, instead of the orders made the past week grouped by customer ID their totals summed together.

Comment by evdubs 2 days ago

Threading macros are nice, though, right?

https://docs.racket-lang.org/threading/introduction.html

Comment by whartung 2 days ago

They're nice, but they're not the same thing.

The threading macros are (as I understand it) pure sugar.

Turning (-> (gather my-list) uppercase-list sort) into (sort (uppercase-list (gather my-list))).

In contrast to, say, Java (I can't speak to the code above):

        List<Things> things = thingIds.stream()
                .map(model::findThing)
                .filter(Objects::nonNull)
                .toList();
These are streamed. This is pretty much a pipe structure, whereas the threading macros will create a lot of temporary copies of the data (I don't know if that's a universal truth). That is, if you're processing a 1000 items, say `gather` returns a 1000 items, that 1000 item list is passed to `uppercase-list` which return a new 1000 item list to feed to `sort` which returns another 1000 item list (assuming none of these are destructive).

I wish CL had something like the Java streams (maybe it does).

Comment by harryposner 2 days ago

Clojure has two options:

The version with a threading macro, will create a lazy-sequence for each step in the pipeline. It will not instantiate the entire list, so it's O(1) memory overhead in terms of peak memory, but it churns O(N) extra garbage.

    (->> things
         (map model/find-thing)
         (filter some?))
And the version with transducers, which will not create any intermediate sequences:

    (sequence (comp (map model/find-thing)
                    (filter some?))
              things)
It looks like there's a Common Lisp transducers library, but I have no idea how widely it's used.

https://github.com/fosskers/transducers

Comment by kagevf 2 days ago

Apparently, the Series library offers that. It didn't make it into the ANSI standard, but it's still maintained and covered in CLtL2.

edit SICP has examples on how to implement streaming (in Scheme).

Comment by evdubs 2 days ago

I am pretty sure Racket's `stream` will handle this use case.

https://docs.racket-lang.org/reference/streams.html

Comment by matheusmoreira 2 days ago

Love those.

Comment by Blikkentrekker 2 days ago

I feel languages should just have some kind of sugar or operator for this, in fact in Ocaml the |> operator exists where

   <exp> |> <exp2>
   <exp2>(<exp>)
Are just one and the same

For a variadic language you'd need something more involved though. But some kind of syntax can probably be invented in some language.

Comment by emidln 2 days ago

It's common to write the thrush combinator as a lisp macro. Clojure ships ->, ->>, as->, some->, some->>, cond->, and cond->> out of the box. You can find similar macros for CL[0], Racket[1], and a scheme SRFI[2]. Writing them is a fun exercise in your lisp of choice if you don't have a library available.

[0] https://github.com/dtenny/clj-arrows

[1] https://docs.racket-lang.org/threading/index.html

[2] https://srfi.schemers.org/srfi-197/srfi-197.html

Comment by sph 2 days ago

Elixir has it. To make it worthwhile, the entire standard library has to be designed to have the ‘object’ of the function as first argument.

   [1,2,3]
   |> Enum.map(&square/1)
   |> Enum.filter(&odd?/1)
Using a threading operator where there is no such consistency is painful. This is why I dislike CL’s or Python’s map function, taking the list to operate on as second argument, instead of first. A threading operator wouldn’t be as effective there.

Comment by shawn_w 2 days ago

Taking the object as the last argument works just as well. Just needs to be consistent whichever way is chosen.

Comment by Blikkentrekker 1 day ago

The issue is that these functions in Lisps are variadic and can accept more arguments than one. `map`, and `zipwidth` in lisps are actually the same function.

Comment by 0x3444ac53 2 days ago

I've never been more thoroughly convinced that I would like ruby more than from this article. I'm currently stuck reaching for python a lot of the time (absolutely love it tbc), but maybe it's worth changing things up and trying to give ruby a shot.

It was one of the first programming languages I was introduced to at 16 or so, but an older person that I looked up to told me it would get me stuck in "hobby coder land". He was wrong in so many ways, but even if he was right, I wanna have fun in my hobby code :)

Comment by Kaliboy 2 days ago

I've had one job in my life, still at the same company. (8 years).

I applied cause the listing mentioned Python, and I was programming in Python at the time.

Once I started they were like yeah we put that there to reach a broader public but we use Ruby (on Rails).

So that's what I learned. I've just returned to Python via LLM's. I literally have not felt the need nor desire to use Python once I got used to writing Ruby.

Comment by NuclearPM 2 days ago

Tbc?

Comment by dmitris 2 days ago

to be clear [IMO]

Comment by 0x3444ac53 19 hours ago

Correct.

Comment by dismalaf 2 days ago

I love Ruby, use it for most of my projects that don't require performance.

Nothing I would love more than a Ruby with a Common-Lisp like compiler and runtime. Unboxed types, native compilation, partial compilation, live image (Ruby has this but "faster Rubies" like Crystal don't), etc...

Comment by vidarh 2 days ago

I have a (self-hosted, but buggy and wildly incomplete; don't try to use - jRuby or TruffleRuby are better - and far faster - options) Ruby compiler that was partly born out of wanting to figure out what this would take, and the answer is it is massively painful because Ruby has failed to take some basic steps that makes delineating read-time and run-time very hard (e.g. you have fun patterns like overriding "require", and iterating over directories to decide what to require) even though most Ruby programs do have clearly separate load and run phases. It's just hard to programmatically separate it.

I still believe you could do pretty well there with a few basic "tricks" that could still also remain real/valid Ruby, by recognising the most common patterns, documenting them, and providing a way of marking exceptions. Combine that with freezing system classes after startup as an enabler for various optimization, and a compiler could do a pretty good job. But it's a massive piece of work to get it right for Ruby.

Comment by Syzygies 2 days ago

I came close to adopting Scala, many parallels to Ruby with vastly better performance.

I'm Ruby or Lean 4.

Comment by rjsw 2 days ago

... or just use Common Lisp.

Comment by dismalaf 2 days ago

Which is what I do. One can dream though right? Of a world where Ruby stayed just a tad more Lisp-y and less Perl/C/Smalltalk/Unix-y.

Also I'm working on a DSL/Macros that give me more Ruby-esque quality of life things in Lisp.

Comment by ralphc 2 days ago

Common Lisp, and even more so Racket, has reader macros. With a little help from LLMs you might be able to get a Ruby-like language that translates into Lisp.

As a last resort look at Racket's "Rhombus" language, it's basically an infix, Python-like syntax on top of Racket. You can use that or see how they pull it off and add Ruby constructs to it.

Comment by tmtvl 2 days ago

Have you checked out dieggsy's Whisper (<https://sr.ht/~dieggsy/whisper/>) yet? It's based on Arne Bab's Wisp (SRFI 119).

Comment by vindarel 1 day ago

and the newer Moonli for CL: https://moonli-lang.github.io/ (:

Comment by evw 2 days ago

For folks that want all of this plus macros (and a lot of other great things), check out Elixir.

Comment by pluralmonad 2 days ago

Elixir has forever ruined me for other languages. Every new PL I dip my toe into gets measured against it. Jose and the core team seem to always land on the right decisions, or at least very good ones.

Comment by ashton314 2 days ago

100% Elixir is much more a Lisp than Ruby is.

Comment by to11mtm 2 days ago

Agree that Elixir is closer to a Lisp than Ruby.

Heck at least in my brain MLs are closer to a Lisp than Ruby...

Comment by neilv 2 days ago

> Both marks come from Scheme, where [...]

Reminds me of an email I wish I still had.

Circa 2000, I wrote that I was leaning towards moving to Scheme, for more rapid R&D work than I could do in Java.

Some nice-sounding person I didn't know emailed me from Japan, to mention a language I hadn't heard of, called Ruby.

I don't know whether the person was Yukihiro Matsumoto himself, but it's a small world.

Comment by muvlon 2 days ago

Most of the points listed are hardly considered lispy anymore these days, Python also has most of these.

Where Ruby's lisp lineage really shows is the fact that it's got Kernel#callcc, aka call with current continuation. It doesn't get any lispier than that!

Comment by t0mpr1c3 1 day ago

`callcc` was obsoleted about 10 years ago. I don't know if it has been removed yet. https://bugs.ruby-lang.org/issues/10548

Comment by insumanth 2 days ago

As I read this, all of my favorite things about ruby compared to something like python were influenced by lisp. Ruby is a joy to program and it seems mostly due to design influence from lisp.

Comment by DonHopkins 2 days ago

What have the Lisps ever done for us?

https://www.youtube.com/watch?v=Qc7HmhrgTuQ

Comment by BoingBoomTschak 2 days ago

Always fun to remind grugs that LISP invented "if" and GC.

Comment by wild_egg 2 days ago

Do you mean blubs?

Comment by jonjacky 2 days ago

Grugs are a different species: https://grugbrain.dev/

Comment by pjmlp 2 days ago

That is actually Lisp influence on Smalltalk, and Perl, that eventually influenced Ruby.

Comment by 0xpgm 2 days ago

From the article

> Matz has said as much. He’s described Ruby’s design as starting from a simple Lisp, stripping out macros and s-expressions, then adding an object system, blocks, and Smalltalk-style methods. The features most Rubyists fall in love with aren’t the object-oriented ones. They’re the functional ones, dressed in friendlier clothes.

Comment by wglb 2 days ago

But macros and s-expressions are two of my favorites parts of lisp!

Comment by dismalaf 2 days ago

Funny enough Lisp was originally meant to be written in a higher level syntax (with infix operators and everything).

But yeah, macros and S-expressions make it easier to write your own DSLs.

Comment by pjmlp 2 days ago

With decades later, Dylan and Julia becoming the only ones that kind of managed to get some adoption doing it.

For better or worse, parenthesis aren't that bad with the proper IDE tooling.

Comment by to11mtm 2 days ago

> For better or worse, parenthesis aren't that bad with the proper IDE tooling.

Hell, even without [0], you can at least count the parenthesis by hand in a pinch. I remember seeing lots of crazy-awesome stuff done in AutoLisp by 'non-programmers', versus 'structure as spacing' in Python which really sucks if the Editor was designed to use the system default (probably non-monospaced, cause other products in the industry had dialogs that broke if you switched to a monospaced) font. [1]

[0] - but real talk parenthesis matching in an editor is a lifesaver

[1] - oooooold version of a very popular GIS product.

Comment by wglb 1 day ago

Reminds me of my first exposure to Lisp. It was in a survey course in my final year. This was using punched card input and output on a line printer. You could turn on paren counting, but it was clumsy at best. This was in 1970

Comment by holg 1 day ago

[dead]

Comment by draegtun 21 hours ago

The article doesn't mention Perl at all but it did have some direct influences on Ruby.

Even Ruby's trailing blocks syntax are an homage to Perl's block list subroutines:

  # first Ruby example in article
  users.select { |u| u.admin? }.map(&:email)

  # using Perl's block list
  map {$_->email} grep {$_->is_admin} @users;

Comment by Smalltalker-80 2 days ago

Totalle agree, I just googled it: "Yukihiro 'Matz' Matsumoto heavily credits Smalltalk as the deepest structural inspiration behind Ruby’s object model. He combined Smalltalk’s beautiful object-oriented architecture and message-passing system with features from other languages to create a tool designed primarily for developer happiness." Including the closures and collection operations.

Comment by riffraff 2 days ago

"Some may say Ruby is a bad rip-off of Lisp or Smalltalk, and I admit that. But it is nicer to ordinary people."

(Matz speaking at the LL2 conference some 20+ years ago)

Comment by dragonwriter 2 days ago

No, its actual influence from Lisp-family languages (including Scheme). Yes, Lisp also influenced Perl and Smalltalk, but Matz was not ignorant of Lisp with the only influence om Ruby from Lisp being indirect through those other languages.

Comment by p_l 2 days ago

Matz directly credits Lisp (through Emacs Lisp) as influence in the design of Ruby and its runtime, with Smalltalk influence on the language itself, and IIRC Perl as "what was popular and we tried to replace"

Comment by danlitt 2 days ago

> He’s described Ruby’s design as starting from a simple Lisp, stripping out macros and s-expressions

Put the macros back! It would be so cool!

Comment by KerrAvon 2 days ago

You kind of don't need them in Ruby, because everything is a method or an object or a closure and you can dynamically create and alter those at runtime. That's why Ruby is really good for ad-hoc DSLs in ways that Rust and Swift really are not.

Comment by yxhuvud 2 days ago

Crystal don't have the dynamicity but has macros to get the next best thing. Most meta magic in Ruby in good code are done at startup anyhow so you don't miss out on that much. YMMV.

Comment by bashkiddie 2 days ago

> because everything is a method or an object or a closure

well, except for pattern matching. That is just syntax.

Comment by matheusmoreira 2 days ago

Macros depend on homoiconicity which Ruby sacrificed in order to have familiar syntax.

Comment by ameliaquining 2 days ago

Homoiconicity makes macros slightly more syntactically elegant, but is not at all necessary. Rust has macros and isn't homoiconic at all.

Comment by matheusmoreira 2 days ago

C has macros too, but it's a second preprocessor language. They both accomplish metaprogramming, but it's questionable whether they're both the same lisplike "macros" we're talking about. Ruby source could be passed through the C preprocessor and get C macros that way. I've actually seen Java code that does just that.

Comment by ameliaquining 2 days ago

C macros are definitely much weaker; they're not by themselves Turing-complete (except maybe with vendor-specific extensions? I'm not an expert here). Rust has both macros by example (precisely analogous to Scheme macros, and equal in power) and procedural macros (conceptually analogous to Common Lisp macros, allowing arbitrary code at macro evaluation time, but I don't know enough about Common Lisp to say whether there are differences in power).

Comment by matheusmoreira 2 days ago

How does it work internally? It would have to output the new source code as data somehow, and have the Rust compiler consume it. How does that happen?

The lispy "macros" I speak of are FEXPRs, just everyday normal functions that just happen to not evaluate their arguments, they receive the source code as lists instead. It's easy to manipulate those lists and evaluate the result.

Lisps themselves moved away from FEXPRs because they were "too powerful" and made the compiler's life hard. Common Lisp and Scheme macros are the more restricted versions that allow compilers to make more assumptions, thereby enabling more aggressive optimization.

Comment by steveklabnik 2 days ago

Rust has two form of macros: “macros by example” and “procedural macros.”

The latter is basically a function from token streams to token streams, and macros by example are more traditional macros which were initially designed by Dave Herman, who was heavily involved in Racket.

Comment by ameliaquining 2 days ago

Yes, a Rust procedural macro is a function that takes a Rust syntax tree as an argument and returns a Rust syntax tree. When you use it, the compiler compiles it (for the host architecture), dynamically loads it into the compiler process, calls it, and inserts the output into the code to be compiled. https://doc.rust-lang.org/book/ch20-05-macros.html#procedura...

I don't see why this would inhibit optimization, unless you mean it slows down compilation, in which case, yep, that's a real and rather notorious downside.

Comment by matheusmoreira 2 days ago

> the compiler compiles it (for the host architecture), dynamically loads it into the compiler process, calls it, and inserts the output into the code to be compiled

That's actually amazing. So the compiler's own data structures are visible in the language.

I see how it works now. Thanks for explaining.

Comment by shawn_w 2 days ago

Put the s-expressions back too.

Comment by somewhereoutth 2 days ago

was this before or after Lisp's epiphany for lexical binding?

Comment by 2 days ago

Comment by tug2024 2 days ago

[dead]