Debuting Monnit, just another monads library

1 Like

I lack standing or qualification to express delight about a monad library. But I have the bias that a good README portends a good product, and monnit’s README is good literature. Mission, adventure, surprise – all the crucial elements. However, the most engaging trick is mystique, the allusion to how your work on various other projects needed monads at every turn. Reading it was like finding myself in the company of people who perceive numbers as colors, or speak naturally in iambic pentameter. I look forward, some day, to reading the higher-level works that stand on the shoulders of this one and breathe an air thick with monads.

2 Likes

Yes true, it be nice in the readme to have some examples of like… Here’s the code made way more readable and simpler due to use of Monnit.

That’s why Fluokitten is my favourite Monad lib for Clojure, it just has this practical element to it. If you read through the getting started Getting Started you can see that it has some functions that would be useful. Want to map or reduce over a Clojure collection but get the same type of collection back instead of a lazyseq? Ok now I already see how fmap and fold can be useful to me. Want to transform the value inside an atom and get the atom back, ok now I see why bind would be useful. Want to apply a series of functions to all elements in a collection, ok now I see why fapply would be useful.

In the case of Monnit, I’m not so sure, are the protocols implemented for the standard Clojure containers already? Why would I want to use the state Monad over just a vector? Etc.

2 Likes

I agree with didibus.

I think the documentation and the README are both nice, but I’m too dumb to know why I’d wanna use this library. To me, the API honestly just looks like the result of a bunch of e.g. (def my-map-fn map) for all the Clojure core functions. Obviously it’s not, but that’s what it looks like when you’re as ignorant as me.

Github would sink to the bottom of the ocean floor if every monad-related project had a great Introduction to Monads in its readme.

I’ll be honest, I’ve yet to find Clojure code written with a monad library that is more readable than “regular” Clojure code. I just looked at the Fluokitten Getting Started (since @didibus mentioned it) and all I see is “clever” code that does things that don’t look much like Clojure semantics to me.

There’s been a monads library in Contrib for many, many years clojure/algo.monads (github.com) with a whole bunch of links to “monad tutorials” by different people – but it’s not idiomatic Clojure to my eyes and this sort of library seems to be used only by a very small number of Clojurians as far as I can tell.

This stuff works well in statically-typed languages – well, it’s pretty much required in those languages – but it’s always felt completely unnecessary in a dynamically-typed language like Clojure.

I’d be very interested to hear from @nilern about the other libraries he’s working on that have all necessitated monads…?

Fair enough, I feel similarly, but specifically to Fluokitten’s point and the reason why it’s the only Monadic lib in Clojure to have caught my attention, the purpose for it is performance, especially with regards to linear algebra.

Clojure’s standard map/reduce won’t be as performant being that it will box everything and turn things into lazy-seqs. By making things Monadic, Fluokitten can always optimize for the best way to handle the given type. With that, it can work on Java primitive arrays and map/reduce them without boxing anything, and return an array as well, or even do an in-place array modification of primitives.

You can use amap and areduce, but being macros they have some downsides, and also they require the use of aget explicitly, where as the Monadic variants can unwrap things automatically.

So Fluokitten really only shines to me from that lens. You would care about the type returned mostly because you’re really wanting to fmap over an array and keep things as arrays, or over some other special container designed for performance. And when you want things to be kept primitive.

Fluokitten is then used in Neanderthal where you have special vector collection types that support primitives as well, and those also can be used with fmap and fold.

So ya, whenever you have collection types that are for performance, Clojure’s standard map/reduce don’t maintain those performance benefits as they will box everything and return a lazyseq. Monads offer a nice alternative here to still have a function pretty similar to map/reduce in generality, while being able to maintain the specific collection’s performance benefits.

The other thing, which are a bit “weird Clojure semantics” is how fmap for example works with maps where it applies the function to each key:

(fmap + {:a 1} {:a 1})
;;> {:a 2}

This doesn’t make sense with map since you’d expect that to return a sequence and + is supposed to be given the map entries themselves not what this fmap is doing.

And maybe that’s a bit not intuitive that it’s different from map in this case. But if you had a linear algebra library that had a Matrix type, and you wanted to multiply the two matrix that would start to make sense that you could have fmap do weird things like this depending on the type.

Which is a niche use case for sure, but still at least that’s the only Monadic library that I was like… Ok ya I might need this one day.

2 Likes

I also find monad libraries not so appealing in general, but I do keep an open mind about them… Some time ago on reddit there was a discussion about this article. For me, using transducers was the obvious next step, but after seeing a comment by Dragan, I also gave Fluokiitten a go…

(defn fast-sq ^double [^double n]
  (* n n))

(defn p+ ^double
  ([^double a ^double b] (+ a b))
  ([^double a] a)
  ([] 0))

(tufte/defnp sum-squares [range-of-nums]
  (->> range-of-nums
     (map fast-sq)
     (reduce p+)))

(tufte/defnp sum-squares-t [range-of-nums]
  (transduce (comp (map fast-sq)) p+ range-of-nums))

(tufte/defnp sum-squares-fk [range-of-nums]
  (f/foldmap p+ 0.0 fast-sq range-of-nums))

(tufte/profile {}
         (dotimes [_ 50]
           (sum-squares (range 10000))
           (sum-squares-t (range 10000))
           (sum-squares-fk (range 10000))))
pId                                          nCalls        Min      50% ≤      90% ≤      95% ≤      99% ≤        Max       Mean   MAD      Clock  Total

:fi.varela.transduce/defn_sum-squares            50   320.94μs   330.57μs   364.40μs   606.02μs     1.68ms     1.68ms   399.14μs  ±29%    19.96ms    53%
:fi.varela.transduce/defn_sum-squares-t          50   170.29μs   177.24μs   212.26μs   363.16μs   822.40μs   822.40μs   210.01μs  ±26%    10.50ms    28%
:fi.varela.transduce/defn_sum-squares-fk         50    99.98μs   103.89μs   116.08μs   193.83μs   240.32μs   240.32μs   113.96μs  ±13%     5.70ms    15%

Accounted                                                                                                                                 36.16ms    96%
Clock                                                                                                                                     37.52ms   100%
2 Likes

The amount of monad tutorials that are out there is legendary, so I did not want to make more. But from the feedback I’ve been getting it looks like the next release will be mostly about tutorial-type documentation. Still, people have such strong opinions about monads et al. that I don’t expect better documentation to change many minds.

Functor and monad instances for collections are incidental to me, while Fluokitten seems to exist mostly to let Neanderthal provide such instances. I use functors and monads to implement higher order control flow and reify effectful computations as values: async IO, parser combinators, effect handlers etc. This has little to do with static typing and everything to do with the fact that Clojure lacks call-with-current-continuation (actually I would like delimited continuations, because call-cc is noncomposable…).

Monadic code is inevitably less readable than direct style (“normal”) code, but with some syntactic sugar it can still be much better than manual CPS; just look at js/Promise (sadly, it does not actually obey the monad invariants). More importantly, category theory abstractions are not leaky and can feel like a higher level language. The Monnit microbenchmark also serves as a very small-scale demonstration of how the state monad makes accumulator plumbing less error prone. Or compare jasca.generic/value to cheshire.parse to see how much simpler it is to use parser combinators than parsing a raw token stream by hand.

PS: It’s cool if you (like me) find my other libraries I linked to here more exciting, but remember they are still unreleased and experimental.

2 Likes

Personally, if I wasn’t clear, my feedback was not for tutorial style documentation, but for a sales pitch. Show me when Monnit would be beneficial, what common code pattern it improves upon, etc. Like would there be benefit for me to lean to Monnit state monads over a transducer next time I need one?

Yes, those all seem more interesting if they pan out. I think that’s maybe circling us back to the point, Clojure Monadic libraries don’t seem to bring a lot of value for your average application developer over standard Clojure facilities. It seems their value is to niche experimental computational libraries or the niche of performance that Neaderthal falls into.

And I want to say, I’m super happy about those niche existing and libraries like those you’re working on and Monnit being released and I find them all very cool and exciting. It’s really awesome that Clojure has that.

Since I’m more of your average application developer though, I think I often find myself outside the target audience maybe, so I tend to want to know, okay but when and why should I pull in Monnit on my next app?

1 Like