Remix.run Logo
hyperrail 7 hours 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.)

tmtvl 2 hours ago | parent | next [-]

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.
evdubs 7 hours ago | parent | prev | next [-]

Threading macros are nice, though, right?

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

whartung 5 hours ago | parent | next [-]

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).

harryposner 3 hours ago | parent | next [-]

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

kagevf 4 hours ago | parent | prev | next [-]

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).

evdubs 4 hours ago | parent | prev [-]

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

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

matheusmoreira 4 hours ago | parent | prev [-]

Love those.

Blikkentrekker 6 hours ago | parent | prev [-]

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.

emidln 6 hours ago | parent [-]

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