├── info.rkt ├── short.rkt ├── unicode.rkt ├── short-test.rkt ├── unicode-test.rkt ├── main-test-rename.rkt ├── LICENSE ├── main-test-default.rkt ├── main.rkt └── README.md /info.rkt: -------------------------------------------------------------------------------- 1 | #lang info 2 | 3 | (define collection "fluent") 4 | (define version "0.9") 5 | (define deps '("base" "rackunit")) 6 | 7 | -------------------------------------------------------------------------------- /short.rkt: -------------------------------------------------------------------------------- 1 | #lang racket/base 2 | 3 | (require "main.rkt") 4 | (provide (except-out (all-from-out "main.rkt") ~> ~~>) 5 | (rename-out (~> >) (~~> >>))) 6 | 7 | -------------------------------------------------------------------------------- /unicode.rkt: -------------------------------------------------------------------------------- 1 | #lang racket/base 2 | 3 | (require "main.rkt") 4 | (provide (except-out (all-from-out "main.rkt") ~> ~~>) 5 | (rename-out (~> →) (~~> ⇒))) 6 | 7 | -------------------------------------------------------------------------------- /short-test.rkt: -------------------------------------------------------------------------------- 1 | #lang racket 2 | 3 | ;; run with raco test 4 | (module+ test 5 | (require "short.rkt" rackunit) 6 | 7 | ;; renamed procedures 8 | (check-equal? (+ 1 1) 2) 9 | (check-equal? (add 1 1) 2) 10 | (check-equal? (subtract 1 1) 0) 11 | (check-equal? (gt? 1 0) #t) 12 | (check-equal? (gte? 1 1) #t) 13 | 14 | ;; lambda shorthand 15 | (check-equal? ((e : #f) 0) #f) 16 | (check-equal? ((e : #t) 0) #t) 17 | (check-equal? ((x y : + x y) 1 2) 3) 18 | 19 | ;; function composition 20 | ("FOO" > string-downcase > check-equal? "foo") 21 | ("FOO" > string-ref (1 > subtract 1) > char-downcase > check-equal? #\f) 22 | ('(1 2 3) >> map (lambda (x) (+ x 1)) > check-equal? '(2 3 4)) 23 | 24 | ;; function composition and lambda shorthand combined 25 | ("FOO" > (x : string-downcase x) > check-equal? "foo") 26 | (((x y : x > add y) 1 2) > check-equal? 3) 27 | ((sort '(3 2 1) (a b : a > lt? b)) > check-equal? '(1 2 3)) 28 | ((sort '(1 2 3) (a b : a > gt? b)) > check-equal? '(3 2 1))) 29 | 30 | -------------------------------------------------------------------------------- /unicode-test.rkt: -------------------------------------------------------------------------------- 1 | #lang racket 2 | 3 | ;; run with raco test 4 | (module+ test 5 | (require "unicode.rkt" rackunit) 6 | 7 | ;; renamed procedures 8 | (check-equal? (+ 1 1) 2) 9 | (check-equal? (add 1 1) 2) 10 | (check-equal? (subtract 1 1) 0) 11 | (check-equal? (gt? 1 0) #t) 12 | (check-equal? (gte? 1 1) #t) 13 | 14 | ;; lambda shorthand 15 | (check-equal? ((e : #f) 0) #f) 16 | (check-equal? ((e : #t) 0) #t) 17 | (check-equal? ((x y : + x y) 1 2) 3) 18 | 19 | ;; function composition 20 | ("FOO" → string-downcase → check-equal? "foo") 21 | ("FOO" → string-ref (1 → subtract 1) → char-downcase → check-equal? #\f) 22 | ('(1 2 3) ⇒ map (lambda (x) (+ x 1)) → check-equal? '(2 3 4)) 23 | 24 | ;; function composition and lambda shorthand combined 25 | ("FOO" → (x : string-downcase x) → check-equal? "foo") 26 | (((x y : x → add y) 1 2) → check-equal? 3) 27 | ((sort '(3 2 1) (a b : a → lt? b)) → check-equal? '(1 2 3)) 28 | ((sort '(1 2 3) (a b : a → gt? b)) → check-equal? '(3 2 1))) 29 | 30 | -------------------------------------------------------------------------------- /main-test-rename.rkt: -------------------------------------------------------------------------------- 1 | #lang racket 2 | 3 | ;; run with raco test 4 | (module+ test 5 | (require (rename-in "main.rkt" [~> >] [~~> >>] [: ::]) rackunit) 6 | 7 | ;; renamed procedures 8 | (check-equal? (+ 1 1) 2) 9 | (check-equal? (add 1 1) 2) 10 | (check-equal? (subtract 1 1) 0) 11 | (check-equal? (gt? 1 0) #t) 12 | (check-equal? (gte? 1 1) #t) 13 | 14 | ;; lambda shorthand 15 | (check-equal? ((e :: #f) 0) #f) 16 | (check-equal? ((e :: #t) 0) #t) 17 | (check-equal? ((x y :: + x y) 1 2) 3) 18 | 19 | ;; function composition 20 | ("FOO" > string-downcase > check-equal? "foo") 21 | ("FOO" > string-ref (1 > subtract 1) > char-downcase > check-equal? #\f) 22 | ('(1 2 3) >> map (lambda (x) (+ x 1)) > check-equal? '(2 3 4)) 23 | 24 | ;; function composition and lambda shorthand combined 25 | ("FOO" > (x :: string-downcase x) > check-equal? "foo") 26 | (((x y :: x > add y) 1 2) > check-equal? 3) 27 | ((sort '(3 2 1) (a b :: a > lt? b)) > check-equal? '(1 2 3)) 28 | ((sort '(1 2 3) (a b :: a > gt? b)) > check-equal? '(3 2 1))) 29 | 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Roger Keays 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /main-test-default.rkt: -------------------------------------------------------------------------------- 1 | #lang racket 2 | 3 | ;; run with raco test 4 | (module+ test 5 | (require "main.rkt" rackunit) 6 | 7 | ;; renamed procedures 8 | (check-equal? (+ 1 1) 2) 9 | (check-equal? (add 1 1) 2) 10 | (check-equal? (subtract 1 1) 0) 11 | (check-equal? (gt? 1 0) #t) 12 | (check-equal? (gte? 1 1) #t) 13 | (check-equal? (&& 1 2) 2) 14 | ;; (check-equal? (&& 1 2 3) 3) ;; not supported 15 | (check-equal? (|| 1 2) 1) 16 | ;; (check-equal? (|| 1 2 3) 1) ;; not supported 17 | 18 | ;; lambda shorthand 19 | (check-equal? ((e : #f) 0) #f) 20 | (check-equal? ((e : #t) 0) #t) 21 | (check-equal? ((x y : + x y) 1 2) 3) 22 | 23 | ;; function composition 24 | ("FOO" ~> string-downcase ~> check-equal? "foo") 25 | ("FOO" ~> string-ref (1 ~> subtract 1) ~> char-downcase ~> check-equal? #\f) 26 | ('(1 2 3) ~~> map (lambda (x) (+ x 1)) ~> check-equal? '(2 3 4)) 27 | 28 | ;; function composition and lambda shorthand combined 29 | ("FOO" ~> (x : string-downcase x) ~> check-equal? "foo") 30 | (((x y : x ~> add y) 1 2) ~> check-equal? 3) 31 | ((sort '(3 2 1) (a b : a ~> lt? b)) ~> check-equal? '(1 2 3)) 32 | ((sort '(1 2 3) (a b : a ~> gt? b)) ~> check-equal? '(3 2 1))) 33 | 34 | -------------------------------------------------------------------------------- /main.rkt: -------------------------------------------------------------------------------- 1 | #lang racket/base 2 | 3 | (require (for-syntax racket/base) syntax/parse/define) 4 | (provide (rename-out [parse #%app] [> gt?] [>= gte?] [< lt?] [<= lte?] [+ add] [- subtract] [* multiply] [/ divide]) 5 | : ~> ~~> && || iterate) 6 | 7 | ;; defining the syntax operators allows them to be renamed using (rename-in) 8 | (define-syntax (: stx) (raise-syntax-error #f "expected param names before lambda shorthand operator" stx)) 9 | (define-syntax (~> stx) (raise-syntax-error #f "expected single form before function composition operator" stx)) 10 | (define-syntax (~~> stx) (raise-syntax-error #f "expected single form before function composition operator" stx)) 11 | 12 | ;; match our parse cases and default to racket/base #%app 13 | (define-syntax-parser parse #:literals (: ~> ~~>) 14 | 15 | ;; infix lambda operator 16 | [(_ (~seq (~and params (~not :)) ...) : atom) #'(lambda (params ...) atom)] 17 | [(_ (~seq (~and params (~not :)) ...) : expr ...) #'(lambda (params ...) (parse expr ...))] 18 | 19 | ;; function composition operators 20 | [(_ data (~seq ~> proc (~and args (~not ~>) (~not ~~>)) ...)) #'(#%app proc data args ...)] 21 | [(_ data (~seq ~> proc (~and args (~not ~>) (~not ~~>)) ...) rest ...) #'(parse (proc data args ...) rest ...)] 22 | [(_ data (~seq ~~> proc (~and args (~not ~>) (~not ~~>)) ...)) #'(#%app proc args ... data)] 23 | [(_ data (~seq ~~> proc (~and args (~not ~>) (~not ~~>)) ...) rest ...) #'(parse (proc args ... data) rest ...)] 24 | 25 | ;; default parser 26 | [(_ rest ...) #'(#%app rest ...)]) 27 | 28 | ;; function composition doesn't work with built-in racket macros and and or, so wrap them in a procedure 29 | (define (&& a b) (and a b)) 30 | (define (|| a b) (or a b)) 31 | 32 | ;; convenience procedures 33 | (define (iterate list proc) (for-each proc list)) 34 | 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # fluent 3 | 4 | UNIX style pipes and a lambda shorthand syntax to make your Racket code more readable. 5 | 6 | ## ? Unpopular So LISP Is Why 7 | 8 | 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 Racket code): 9 | 10 | UNIX: cat data.txt | grep "active" | sort | uniq 11 | LISP: (remove-duplicates (sort ((filter (λ (line) (string-contains? line "active")) (file->lines "data.txt"))))) 12 | 13 | Using *fluent*, the same Racket code can be written according to the UNIX philosophy: 14 | 15 | ("data.txt" ~> file->lines ~~> filter (line : line ~> string-contains? "active") ~> sort ~> remove-duplicates) 16 | 17 | As you'll see from the examples above, *fluent* adds support for function composition using `~>` and `~~>`, and lambda functions using `:`. If you don't like this syntax, *fluent* allows you to define your own operators using `(rename-in)` or choose from some predefined alternatives. E.g: 18 | 19 | (require fluent/unicode) 20 | ("data.txt" → file->lines ⇒ filter (line : line → string-contains? "active") → sort → remove-duplicates) 21 | 22 | ## Function Composition 23 | 24 | Using the function composition operator, `~>`, *fluent* inserts the left hand side as the first parameter to the procedure on the right hand side. Use `~~>` to add the left hand side as the last parameter to the procedure. 25 | 26 | (data ~> procedure params) becomes (procedure data params) 27 | (data ~~> procedure params) becomes (procedure params data) 28 | 29 | This operation can be chained or nested as demonstrated in the examples. 30 | 31 | ## Lambda Shorthand 32 | 33 | 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: 34 | 35 | > ((x : + x 1) 1) 36 | 2 37 | > ((x y : + x y) 1 2) 38 | 3 39 | > (map (x : string-upcase x) '("a" "b" "c")) 40 | '("A" "B" "C") 41 | 42 | ## Flexible Syntax 43 | 44 | If you don't like the default operators, you can rename then using (rename-in): 45 | 46 | (require (rename-in fluent (~> >) (~~> >>))) 47 | ("hello world" > string-split >> map string-upcase) ;; '("HELLO" "WORLD") 48 | 49 | *fluent* comes with two built-in alternative syntaxes, `fluent/short` (which uses `>` and `>>`) and `fluent/unicode` (which uses `→` and `⇒`): 50 | 51 | (require fluent/short) 52 | ("hello world" > string-split >> map string-upcase) ;; '("HELLO" "WORLD") 53 | 54 | (require fluent/unicode) 55 | ("hello world" → string-split ⇒ map string-upcase) ;; '("HELLO" "WORLD") 56 | 57 | `→` is Unicode character 2192. On linux you can enter this using `shift-ctrl-u 2192 enter`. `⇒` is 21D2. Naturally, if you want to use these characters, you should map it to some unused key on your keyboard. This can be done with xmodmap: 58 | 59 | # use xev to get the keycode 60 | $ xev 61 | 62 | # check the current mapping 63 | $ xmodmap -pke 64 | 65 | # replace the mapping 66 | $ xmodmap -e "keycode 51=U2192 U21D2 ccedilla Ccedilla braceright breve braceright" 67 | 68 | This maps the cedilla key to `→`, and shift-cedilla to `⇒`. You could use the menu key, window key, pause, insert, caps lock, or any other useless key on your keyboard. Making this change permanent depends on your session manager. Search duckduckgo for details. 69 | 70 | ## Convenience Procedures 71 | 72 | When using function composition, procedures with text names are easier to read. For this reason, *fluent* provides the following alternative names for the common base math procedures. 73 | 74 | > gt? 75 | < lt? 76 | >= gte? 77 | <= lte? 78 | + add 79 | - subtract 80 | * multiply 81 | / divide 82 | 83 | Compare: 84 | 85 | (1 ~> + 1 ~> * 2 ~> >= 3) 86 | (1 ~> add 1 ~> multiply 2 ~> gte? 3) 87 | 88 | Of course, the choice is yours. Note, if you use *fluent/short* you will need to use `gt?` for the math procedure, as `>` is used for function composition. 89 | 90 | *fluent* also works best with procedures which place the data parameter 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: 91 | 92 | original data-first version 93 | ----------------------------- 94 | for-each iterate 95 | 96 | example: 97 | 98 | > ('(1 2 3) ~> iterate (x : displayln x)) 99 | 1 100 | 2 101 | 3 102 | 103 | ## Comparison to Clojure's Threading Macro 104 | 105 | *fluent* uses infix operators which has three main advantages over other prefix macros you'll find for Clojure, Racket and LISP. Firstly, you can combine `~>` and `~~>` just fine without using any ugly hacks: 106 | 107 | ("hello world" ~> string-upcase ~~> regexp-match? #px"LL" ~> eq? #t) 108 | 109 | Secondly, you don't need to put parentheses around procedures that take additional parameters. You can see this at work in the last two functions in the example above and in the example below. 110 | 111 | Finally, infix operators make nested code easier to follow. Compare: 112 | 113 | CLOJURE (prefix): 114 | 115 | (-> (list (-> id3 (hash-ref 'genre "unknown")) 116 | (-> id3 (hash-ref 'track "0")) 117 | (-> id3 (hash-ref 'artist "unknown")) 118 | (-> id3 (hash-ref 'title "unknown"))) 119 | (string-join ".")) 120 | 121 | FLUENT (infix): 122 | 123 | (list (id3 ~> hash-ref 'genre "unknown") 124 | (id3 ~> hash-ref 'track "0") 125 | (id3 ~> hash-ref 'artist "unknown") 126 | (id3 ~> hash-ref 'title "unknown")) ~> string-join ".") 127 | 128 | And of course, with *fluent* you can use your own syntax. 129 | 130 | ## Installation 131 | 132 | This library is available from the Racket package collection and can be installed with raco: 133 | 134 | $ raco pkg install fluent 135 | 136 | All you need to do is `(require fluent)`. You can try it out in the REPL: 137 | 138 | > (require fluent) 139 | > ("FOO" ~> string-downcase) 140 | "foo" 141 | > ((x y : x ~> add y) 1 2) 142 | 3 143 | 144 | ## Known Issues 145 | 146 | The function composition operators are not compatible with the `and` and `or` macros. *fluent* provides wrapper `&&` and `||` procedures which work with two arguments. 147 | 148 | ## Related Resources 149 | 150 | For more solutions to your life's problems, [visit the author's website](https://rogerkeays.com). 151 | 152 | --------------------------------------------------------------------------------