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.
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.
So I might naturally write code:
f = open("some/file.path")
if f == ERROR_FILE_NOT_FOUND:
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.
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.
1. Lisp code is hard to read.
2. Lisp based languages do not have "small syntax".
f x |> Seq.map g |> Seq.filter h |> Seq.groupBy (fun x -> x.Name)
Seq.groupBy (fun x -> x.Name) (Seq.filter h (Seq.map g (f x)))
(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)
clojure programmers don't know how to read lisp code.