< B / >

Threading Macros And Pipes

Started 2 days ago Last edited 24 minutes ago

≺≻ 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 response

A compiler is this:

Source code -compiler-> Target Language Code

And a file browser window just this:

folder path -file browser-> folder view

When 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:

main.ts
export const main = (request) => {
return formatResponse(validateRequest(parseHeaders(request)))
}
main.ex
def main(request) do
format_response(validate_request(parse_headers(request)))
end
main.hs
main :: Request -> Response
main req = formatResponse(validateRequest(parseHeaders req))
main.rkt
(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:

main.ts
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)
}
main.ex
def main(request) do
request
|> parse_headers
|> validate_request
|> format_response
end
main.hs
main :: Request -> Response
main req = req & parseHeaders & validateRequest & formatResponse
main.rkt
(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:
main.ex
def main(request) do
request
|> parse_headers
|> IO.inspect()
|> validate_request
|> format_response
end
  • 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:
example.el
(-as-> topic-string _
(string-split _ "," t " ")
(mapcar (lambda (topic) (concat "#" topic)) _)
(string-join _ " "))