fluent: unix style pipes and lambda shorthand to make your code more readable

111 views
Skip to first unread message

Roger Keays

unread,
Mar 9, 2021, 10:20:42 AM3/9/21
to racket...@googlegroups.com
Hi all,

I recently publish a new package called *fluent* which adds some syntax enhancements to Racket. Links and README below. Let me know what you think...

Roger

https://pkgs.racket-lang.org/package/fluent
https://github.com/rogerkeays/racket-fluent/

# fluent

UNIX style pipes and a lambda shorthand syntax to make your Racket code more readable.

## ? Unpopular So LISP Is Why

Let's be honest. LISP missed a huge opportunity to change the world by telling developers they have to think backwards. Meanwhile, UNIX became successful largely because it allows you to compose programs sequentially using pipes. Compare the difference (the LISP example is actually racket):

UNIX: cat data.txt | grep "active" | cut -f 4 | uniq | sort
LISP: (sort (remove-duplicates (map (λ (line) (list-ref (string-split line) 4)) ((filter (λ (line) (string-contains? line "active")) (file->lines "data.txt"))))))

Using *fluent*, the same racket code can be written according to the UNIX philosophy:

("data.txt" > file->lines >> filter (line : line > string-contains? "active") >> map (line : line > string-split > list-ref 4) > remove-duplicates > sort)

You can use unicode → instead of > if you prefer. It is more distinctive and a bit easier on the eyes:

("data.txt" → file->lines →→ filter (line : line → string-contains? "active") →→ map (line : line → string-split → list-ref 4) → remove-duplicates → sort)

## Function Composition

Using the function composition operator (> or →), *fluent* inserts the left hand side as the first parameter to the procedure on the right hand side. Use >> (or →→) to add the left hand side as the last parameter to the procedure.

(data > procedure params) becomes (procedure data params)
(data >> procedure params) becomes (procedure params data)

This operation can be chained or nested as demonstrated in the examples.

## Lambda Shorthand

The : operator allows you to easily write a lambda function with one expression. Parameters go on the left, the expression on the right, no parentheses required. For example:

> ((x : + x 1) 1)
2
> ((x y : + x y) 1 2)
3
> (map (x : string-upcase x) '("a" "b" "c"))
'("A" "B" "C")

## Math Procedures

Since this library uses > for function composition, the built in greater-than procedure is renamed to `gt?`. Note, this could break existing code if you are already using the > procedure. Other math procedures are also renamed for consistency, and because the text versions read more naturally when using function composition.

> gt?
< lt?
>= gte?
<= lte?
+ add
- subtract
* multiply
/ divide

## Convenience Procedures

*fluent* works best when the data (input) parameter comes first. Most racket functions do this out of the box, but many functions which take a procedure as a parameter put the data last. That's fine, because you can just use >>. Alternatively you can wrap and rename the procedure, which is what we've done for these functions:

original data-first version
-----------------------------
for-each iterate

example:

> ('(1 2 3) → iterate (x : displayln x))
1
2
3

## Comparison to Clojure's Threading Macro

Clojure's threading macro is a prefix operator, which means it is less readable when nested and requires more parentheses. You could say that the *fluent* infix operator acts as one parenthesis. Compare:

CLOJURE (prefix):

(-> (list (-> (-> id3 (hash-ref 'genre "unknown")) normalise-field)
(-> (-> id3 (hash-ref 'track "0")) normalise-field)
(-> (-> id3 (hash-ref 'artist "unknown")) normalise-field)
(-> (-> id3 (hash-ref 'title "unknown")) normalise-field)) (string-join "."))

FLUENT (infix):

(list (id3 → hash-ref 'genre "unknown" → normalise-field)
(id3 → hash-ref 'track "0" → normalise-field)
(id3 → hash-ref 'artist "unknown" → normalise-field)
(id3 → hash-ref 'title "unknown" → normalise-field)) → string-join ".")

Fluent's infix approach also makes it easier to combine thread-first (→) with thread-last (→→).

## How to enter → with your keyboard

→ is Unicode character 2192. On linux you can enter this using `shift-ctrl-u 2192 enter`. Naturally, if you want to use this character, you should map it to some unused key on your keyboard. This can be done with xmodmap:

# use xev to get the keycode
$ xev

# check the current mapping
$ xmodmap -pke

# replace the mapping
$ xmodmap -e "keycode 51=U2192 Ccedilla ccedilla Ccedilla braceright breve braceright"

Making this change permanent depends on your session manager. Search duckduckgo for details.

## Installation

This library is available from the Racket package collection and can be installed with raco:

$ raco pkg install fluent

All you need to do is `(require fluent)`. You can try it out in the REPL:

> (require fluent)
> ("FOO" > string-downcase)
"foo"
> ((x y : x > add y) 1 2)
3

Jay McCarthy

unread,
Mar 9, 2021, 10:40:35 AM3/9/21
to Roger Keays, Racket Users
I like this a lot! Great job!

--
Jay McCarthy
Associate Professor @ CS @ UMass Lowell
http://jeapostrophe.github.io
Vincit qui se vincit.
> --
> You received this message because you are subscribed to the Google Groups "Racket Users" group.
> To unsubscribe from this group and stop receiving emails from it, send an email to racket-users...@googlegroups.com.
> To view this discussion on the web visit https://groups.google.com/d/msgid/racket-users/sigid.1702887e81.20210309152029.GA3105%40papaya.papaya.

David Storrs

unread,
Mar 9, 2021, 12:56:42 PM3/9/21
to Roger Keays, Racket Users
This is very cool.  You might take a look at the `threading` module for additional ideas: https://docs.racket-lang.org/threading/index.html

On Tue, Mar 9, 2021 at 10:20 AM Roger Keays <rac...@rogerkeays.com> wrote:

Tim Lee

unread,
Mar 9, 2021, 2:28:17 PM3/9/21
to Roger Keays, racket...@googlegroups.com
> Using *fluent*, the same racket code can be written according to the UNIX philosophy:
>
> ("data.txt" > file->lines >> filter (line : line > string-contains? "active") >> map (line : line > string-split > list-ref 4) > remove-duplicates > sort)

This reminds me of Clojure's threading macros (->, ->>, etc.), and
OCaml's "reverse-application operator": "|>", where x |> f |> g is
equivalent to (g (f x)). The tacking-on of such convenience
functions/syntax across so many functional languages is probably a sign that
many of these languages have it backwards (from a user-friendliness point of
view).

Daniel Prager

unread,
Mar 9, 2021, 7:39:42 PM3/9/21
to Racket Users
Impressive!

How does fluent manage this infixing from a (require ...) rather than a #lang?

I've been using the Clojure-like threading package for a while now and this has some nice advantages that are mentioned in the docs, like blending the first arg > and last arg >> variants easily in a sequence.

How does fluent manage this infixing from a (require ...) rather than a #lang?

It might be nice to use ~> and ~>> (or |> and |>> or choose your own) as infix to avoid clashing with >.


Dan

Roger Keays

unread,
Mar 10, 2021, 8:23:57 AM3/10/21
to Daniel Prager, racket...@googlegroups.com
> I've been using the Clojure-like threading package for a while now and this has
> some nice advantages that are mentioned in the docs, like blending the first
> arg > and last arg >> variants easily in a sequence.
>
> How does fluent manage this infixing from a (require ...) rather than a #lang?

It provides a custom #%app macro which rearranges the code into regular s-expressions. I was surprised by how much flexibility this feature of racket gives you. It would have taken months to build this from scratch in any other language. The racket version is 25 lines of code. [1]

> It might be nice to use ~> and ~>> (or |> and |>> or choose your own) as infix
> to avoid clashing with >.

I wanted a single character because I use it so often. I suppose with the unicode (→) option, there is less need for a single-character solution. Need to think about this...


[1] https://github.com/rogerkeays/racket-fluent/blob/main/main.rkt
>
>
> Dan
>
> --
> You received this message because you are subscribed to the Google Groups
> "Racket Users" group.
> To unsubscribe from this group and stop receiving emails from it, send an email
> to racket-users...@googlegroups.com.
> To view this discussion on the web visit https://groups.google.com/d/msgid/
> racket-users/
> CAFKxZVVBV1sP%2BKk%2B%2Bdw7k7a_TcEsL-yz6%2BLKDLZOa2Wfc1RAYw%40mail.gmail.com.

Roger Keays

unread,
Mar 10, 2021, 8:36:59 AM3/10/21
to David Storrs, Roger Keays, Racket Users
> This is very cool.  You might take a look at the `threading` module for
> additional ideas: https://docs.racket-lang.org/threading/index.html

Yeh, the threading macro is what took me down this rabbit-hole. But I wanted an infix operator, mostly because of my pernicious unix habit. I found there are some other advantages too. You need fewer parens, can mix thread-first and thread-last, and it makes reading nested operations easier. I don't think I'll go back to prefix :)

I even experiment with making the whole language infix... as in (data proc params). It was interesting, but ultimately I concluded that prefix should be the default, and infix should be the special case.

Roger Keays

unread,
Mar 10, 2021, 8:41:48 AM3/10/21
to Tim Lee, racket...@googlegroups.com

Hendrik Boom

unread,
Mar 10, 2021, 7:31:14 PM3/10/21
to racket...@googlegroups.com
On Wed, Mar 10, 2021 at 08:23:46PM +0700, Roger Keays wrote:
> > I've been using the Clojure-like threading package for a while now and this has
> > some nice advantages that are mentioned in the docs, like blending the first
> > arg > and last arg >> variants easily in a sequence.
> >
> > How does fluent manage this infixing from a (require ...) rather than a #lang?
>
> It provides a custom #%app macro which rearranges the code into regular s-expressions. I was surprised by how much flexibility this feature of racket gives you. It would have taken months to build this from scratch in any other language. The racket version is 25 lines of code. [1]
>
> > It might be nice to use ~> and ~>> (or |> and |>> or choose your own) as infix
> > to avoid clashing with >.
>
> I wanted a single character because I use it so often. I suppose with the unicode (→) option, there is less need for a single-character solution. Need to think about this...

Only if your keyboard has a convenient single key to type it with.

-- hendrik

>
>
> [1] https://github.com/rogerkeays/racket-fluent/blob/main/main.rkt
> >
> >
> > Dan
> >
> > --
> > You received this message because you are subscribed to the Google Groups
> > "Racket Users" group.
> > To unsubscribe from this group and stop receiving emails from it, send an email
> > to racket-users...@googlegroups.com.
> > To view this discussion on the web visit https://groups.google.com/d/msgid/
> > racket-users/
> > CAFKxZVVBV1sP%2BKk%2B%2Bdw7k7a_TcEsL-yz6%2BLKDLZOa2Wfc1RAYw%40mail.gmail.com.
>
> --
> You received this message because you are subscribed to the Google Groups "Racket Users" group.
> To unsubscribe from this group and stop receiving emails from it, send an email to racket-users...@googlegroups.com.
> To view this discussion on the web visit https://groups.google.com/d/msgid/racket-users/sigid.0703ba7d55.20210310132341.GA3081%40papaya.papaya.

Roger Keays

unread,
Mar 11, 2021, 11:20:36 AM3/11/21
to Matthias Felleisen, racket...@googlegroups.com
On Wed, Mar 10, 2021 at 09:44:15AM -0500, Matthias Felleisen wrote:
>
> Hi, are you aware of rash? Different goals but perhaps a merger of your package with rash would give us a heck of a new shell :-)

Hey, I took a look at rash, but racket (with fluent) turned out to be better suited to my needs. I definitely find myself turning to racket more often now for tasks I would have normally done with bash (which was the whole point). Racket startup time is quite a bit slower, but I got over it. Not sure if I'd take on the bold task of releasing a new shell though.
> > --
> > You received this message because you are subscribed to the Google Groups "Racket Users" group.
> > To unsubscribe from this group and stop receiving emails from it, send an email to racket-users...@googlegroups.com.
> > To view this discussion on the web visit https://groups.google.com/d/msgid/racket-users/sigid.1702887e81.20210309152029.GA3105%40papaya.papaya.
>

Roger Keays

unread,
Mar 11, 2021, 1:44:28 PM3/11/21
to Hendrik Boom, racket...@googlegroups.com
> > > It might be nice to use ~> and ~>> (or |> and |>> or choose your own) as infix
> > > to avoid clashing with >.

I'm thinking about changing the default operators to ~> and ~~> and making the unicode versions available using (require fluent/unicode). I actually prefer the long arrow for thread-last to the current syntax (>>).

Any thoughts?
> To view this discussion on the web visit https://groups.google.com/d/msgid/racket-users/20210311003108.qjqsrn4kd6nbwtd5%40topoi.pooq.com.

schle...@gmail.com

unread,
Mar 12, 2021, 7:31:08 AM3/12/21
to Racket Users
I am not sure about the technical details, but would it be possible to rename those identifiers with?: (require (rename-in fluent [→ ~>] [→→ ~~>]))
I think if you define those arrows as syntax parameters they could be renamed, but I don't know whether that is the "right" way to do it in racket.
https://docs.racket-lang.org/reference/stxparam.html?q=syntax-parameter

I like the ~> and ~~> arrows they are quite distinct, seems like a good default to me.
I think the rackety way would be to have one default name and make them rename-able.
Then fluent/unicode would not be necessary, but I also would not be bothered by it.

On a technical level I would prefer a solution that does not result in more and more code generation for every renamed variant.

Roger Keays

unread,
Mar 12, 2021, 1:58:06 PM3/12/21
to schle...@gmail.com, Racket Users
On Fri, Mar 12, 2021 at 04:31:07AM -0800, schle...@gmail.com wrote:
> I am not sure about the technical details, but would it be possible to rename
> those identifiers with?: (require (rename-in fluent [→ ~>] [→→ ~~>]))
> I think if you define those arrows as syntax parameters they could be renamed,
> but I don't know whether that is the "right" way to do it in racket.
> https://docs.racket-lang.org/reference/stxparam.html?q=syntax-parameter

AFAICT, syntax-parse doesn't resolve syntax-parameters. I can parameterise the parse pattern using syntax classes, but those can't be changed using rename-in.

There is probably a solution to this problem. I'm just not seeing it yet.
> msgid/racket-users/sigid.0703ba7d55.20210310132341.GA3081%40papaya.papaya.
> >
> > --
> > You received this message because you are subscribed to the Google Groups
> "Racket Users" group.
> > To unsubscribe from this group and stop receiving emails from it, send an
> email to racket-users...@googlegroups.com.
> > To view this discussion on the web visit https://groups.google.com/d/
> msgid/racket-users/20210311003108.qjqsrn4kd6nbwtd5%40topoi.pooq.com.
>
> --
> You received this message because you are subscribed to the Google Groups
> "Racket Users" group.
> To unsubscribe from this group and stop receiving emails from it, send an email
> to racket-users...@googlegroups.com.
> To view this discussion on the web visit https://groups.google.com/d/msgid/
> racket-users/79e3eb5f-90cb-49fb-824b-92e5b184053en%40googlegroups.com.

Sorawee Porncharoenwase

unread,
Mar 12, 2021, 2:09:57 PM3/12/21
to Roger Keays, schle...@gmail.com, Racket Users

There is probably a solution to this problem. I'm just not seeing it yet.

Yeah, syntax parameters are not really relevant here.

A way to make rename-in work is to define-syntax the “token” to a dummy syntax transformer, and change your macro to recognize the token using ~literal. Here’s an example:

#lang racket

(module foo racket
  (require syntax/parse/define)
  (define-syntax (:= stx)
    (raise-syntax-error #f "out of context" stx))
  (define-syntax-parse-rule (def x {~literal :=} e)
    (define x e))
  (provide def :=))

(require (rename-in 'foo [:= ←]))

(def x ← 5)
x
 

Reply all
Reply to author
Forward
0 new messages