Hacker News new | past | comments | ask | show | jobs | submit login
Threading Macros Guide (clojure.org)
83 points by tosh 24 days ago | hide | past | web | favorite | 15 comments



Threading macros make it very obvious how nested functions are competitive with imperative styles. They both make the problems more obvious and the solutions to those problems more intuitive.

After using them for a few hours, suddenly monads become obvious. The values passing through the thread need a level of boxing; and the threading macro can respond sensibly when errors occur.

Continuing the logical pattern from -> to some-> there is a fertile field of possibility for threading macros to provide the error handling structures that get lost moving from imperative to functional programming. Probably possible to be too clever though.


> After using them for a few hours, suddenly monads become obvious

They do? I mean, I love the pipe operator from F# and the thread-first macro in Clojure, but I'm missing the part where suddenly it's monads.


Possibly I'm about to reveal a horrible misunderstanding of monads :p.

So I might naturally write code:

  f = open("some/file.path")

  if f == ERROR_FILE_NOT_FOUND:
    print("ERROR_FILE_NOT_FOUND")
    return -1

  f.write("yes")
vs with a threading macro

  (-> "some/file.path"
      (open)
      (write "yes"))
Obviously I've lost the test in the middle and the thread macro code will crash hard if something goes wrong. I need to fix it. Now I could re-write (open) so that it handles errors internally. We don't really want that because probably how we handle errors is context dependent. However, we have a function that is basically the context: (->)! So instead of embedding the error handling by re-writing (open) we embed it into (->) by rewriting it as (-$>).

So now we have (-$>) that checks every step of the thread and handles ERROR_FILE_NOT_FOUND appropriately. And returns the -1 we were looking for. Neat.

However, now we are in territory where we would like to start using an object that is either in a "successful" state or a "failed" state, and that is quite primitive so that (-$>) knows about it. Ie, we want (-$>) to appear in a library that doesn't know anything about our specific use case, and generalises beyond ERROR_FILE_NOT_FOUND. We are about to stumble into something very similar to Haskell's Maybe monad.

So we created a new problem - can't check state in the middle of a nested function call - and the solution is to use a threading macro that by-the-way does error checking and thinks in monads.


Ah yep, that makes sense. Thanks for the detailed explanation :)

Actually, you just reminded me of my recent experience of delving into Go -I wrote a simple script to read in n YAML files from a directory and merge them into one, we're using the Prometheus JMX exporter which only accepts one YAML file, but using several smaller, more focused configs kept the complexity down.

I used Go to produce a standalone binary so that everything needed could be brought in from a tiny Docker image as a mixin. Go's default of building standalone binaries is what tempted me, it was far easier to use Go in a multi-stage build to create a static binary than it was to try to provide the same using Python etc.

But yeah, opening a directory, reading its contents, and then consuming all the files within it required five "if err != nil" blocks. But all I really needed was for an error in stage N of the chained IO operations to propagate through the pipeline so that the result of using the pipeline was Some(x) or None.



I really enjoy the Failjure library [1] to make threads that short-circuits on failures. This is pretty close to what monadic error handling looks like in strongly typed functional languages.

[1] https://github.com/adambard/failjure


Having no experience with clojure, this reminded me of pipeable RxJS syntax, as well as more generally the sometimes seen use of _ to denote an unused “threaded value” in more functional styles of js (although _ commonly comes as the final value in js in combination with unary functions, or will sometimes be named but unused to assist in establishing context for readability).


Also check out the Swiss arrows project which builds on the idea a bit. https://github.com/rplevy/swiss-arrows


not sure why i would use that vs this

https://clojuredocs.org/clojure.core/as-%3E


Prevalent use of these macros in clojure code is admission to the following facts,

1. Lisp code is hard to read.

2. Lisp based languages do not have "small syntax".


Nothing to do with Lisp code, because otherwise F# wouldn't have a pipe operator.

  f x |> Seq.map g |> Seq.filter h |> Seq.groupBy (fun x -> x.Name)
Is just more explicit and obvious than

  Seq.groupBy (fun x -> x.Name) (Seq.filter h (Seq.map g (f x)))


I don't know much about F# but do you know if language has to be "expanded" to support that pipe operator?


LMFTFY:

(one Lisp code is hard to read for people who don't read lisp code.)

(two Lisp based languages have tree-like structures with a simple syntax)


which would imply

clojure programmers don't know how to read lisp code.


Facts?




Applications are open for YC Winter 2020

Guidelines | FAQ | Support | API | Security | Lists | Bookmarklet | Legal | Apply to YC | Contact

Search: