Threading Macros And Pipes
≺≻ Modeling Programs as Functions
In functional programming, we try to model our programs around the idea of functions that take in inputs and return outputs.
A webserver, for example, is just this:
HTTP request -webserver-> HTTP responseA compiler is this:
Source code -compiler-> Target Language CodeAnd a file browser window just this:
folder path -file browser-> folder viewWhen we go through with this, a big chunk (except for the part at ther boundary where effects should happen) of our program is just Function Composition.
When we apply this idea naively, our programs look like this:
export const main = (request) => { return formatResponse(validateRequest(parseHeaders(request)))}def main(request) do format_response(validate_request(parse_headers(request)))endmain :: Request -> Responsemain req = formatResponse(validateRequest(parseHeaders req))(define (main request) (format-response (validate-request (parse-headers request)))) This is okay (and still waay better that a ton of statements, I’d say), but not it loses the nice property of statements that it’s not nested.
Also, when we try to read/understand the code, we’re probably going to read it from right to left (while mentally ‘piping’ through the arguments in our heads).
If we mentally parse the code like this either way, we can also just write it down like that:
import { pipe } from 'effect' // for example, but it's pretty easy to write yourself, too
export const main = (request) => { return pipe(request, parseHeaders, validateRequest, formatResponse)}def main(request) do request |> parse_headers |> validate_request |> format_responseendmain :: Request -> Responsemain req = req & parseHeaders & validateRequest & formatResponse(require threading)
(define (main request) (~> request parse-headers validate-request format-response))Two more sidenotes:
- I think pipelines are especially useful when paired with something like Elixir’s
IO.inspect(), like this:
def main(request) do request |> parse_headers |> IO.inspect() |> validate_request |> format_responseend- Pipelines need their functions to be functions of only one arg (in ‘effect’, for example), or they need to make a choice on where to splice the argument into the function call. Elixir always splices it into the first, while Lisps aren’t decided that (decide per-case), and normally have both a
->(inserts as the first argument),->>(inserts as the last argument), as well as-as->-variants, which you can call like this:
(-as-> topic-string _ (string-split _ "," t " ") (mapcar (lambda (topic) (concat "#" topic)) _) (string-join _ " "))