[HN Gopher] Monads for Clojure Programmers
___________________________________________________________________
Monads for Clojure Programmers
Author : Terretta
Score : 53 points
Date : 2021-10-04 10:32 UTC (1 days ago)
(HTM) web link (cuddly-octo-palm-tree.com)
(TXT) w3m dump (cuddly-octo-palm-tree.com)
| adamgordonbell wrote:
| A monad, as thought of in programming, is really something that
| requires higher kinded types.
|
| The reason they are hard to explain in a language without HKT is
| the same reason why List[T] would be hard to explain in a
| language without generics.
|
| edit: The article is impressive. It's just it feels like a
| different thing to me to than what monads are in haskell. I guess
| it would have to be.
| mping wrote:
| You can have monads in clojure (eg:
| http://funcool.github.io/cats/latest/#monad) but the problem I
| see is that they are infectious and kinda force you to use them
| everywhere. In other languages, it's more palatable because the
| compiler checks for types, and normally there is some sugar to
| make it easier on the eyes. Besides, I think monads are a bit
| off the clojure way, even if they happen to exist somehow (eg
| promesa and manifold libraries).
| goostavos wrote:
| And _without_ the compiler checking the types, they 're
| nightmarish to use. It falls to pure human sweat to track if
| a variable holds a `thing` or a `Monad Thing`. In my humble
| experience, it leads to lots of tedious banging around in the
| REPL to track down where you made a mistake. This is
| especially painful while refactoring.
|
| I've given up on the monads in dynamic languages outside of
| really strict confined situations (validation for instance).
| I think it's ultimately a bit silly to do something like
| `StateT` in Clojure rather than just using an `atom` or
| whatever. What you lose in referential transparency and
| purity, you more than make up for in sanity over the long
| haul.
|
| The scales would tip if Clojure had stronger typing options,
| but for now, using the idiomatic stuff is the way to go.
| Although, spec could probably be used in a pinch, but it's
| kind've hot garbage compared to compiler checked types
| (imho!).
| elwell wrote:
| I love your podcast.
| adamgordonbell wrote:
| Thanks!
| [deleted]
| platz wrote:
| It's not that monads "require" higher kinded types, it's that ,
| like an interface, monads only start really "buying" their
| "cost" when there are _many_ different things that can be
| monads.
|
| Then the common abstraction of the monad can be used to
| interact with these things that all share this common property,
| in a unified way.
|
| Finally, higher kinded types is what makes the idea of _many_
| different things that can be monads manageable for the
| programmer, because if the underlying thing that the monad
| represents keeps changing into different forms, it's very easy
| to loose track of what structure or concept it currently
| represents without the types to keep track of that for you.
| adamgordonbell wrote:
| yeah, that's better put.
|
| The usefulness of it, to me, is how the bind/flatmap can be
| generic over type constructors, but type constructors aren't
| a clojure concept. It's a static typing thing.
|
| To have an interface for monads, you need to have HKT don't
| you? To declare the type of bind?
|
| It could be useful otherwise. It would just seem like a
| slightly different thing.
| fiddlerwoaroof wrote:
| Someone once told me "continuations are isomorphic to
| monads" and I eventually realized that the best way to do
| something monad-like in JS was to write functions that take
| a callback as their last argument and then higher order
| functions that manage threading the callbacks around,
| occasionally transforming it to slot in impure behaviors.
|
| I really disagree with the notion "HKT are necessary for
| monads", though: they're necessary to statically check that
| your monadic code is correct, but asside from things that
| rely on return-type inference, you can just implement those
| type-checks as runtime checks (or leave them out entirely):
| if the architecture of side-effects in your app is arranged
| around monadic principles, you can write all the same
| generic transformations (and, in fact, designing your
| dynamically typed code this way can get you a good fraction
| of the correctness benefits of static typechecking)
| fiddlerwoaroof wrote:
| One example here. CLOS-style multimethods are a
| reasonably good runtime analog of the way typeclasses
| work: typeclasses use type inference to thread a
| dictionary of functions around, multimethods do roughly
| the same thing with dynamic type-checks. This means
| multimethods have overhead typeclasses don't have, but it
| also means that multimethods don't have the "closed-
| world" assumption and can be extended dynamically.
| jerf wrote:
| You can also theoretically go the completely opposite
| direction and use dynamic typing. You have the usual
| tradeoffs related to that, plus maybe a couple new
| wrinkles, but it can work.
| jerf wrote:
| I think there's a lot of people confusing the bricks of
| functional programming with what you can build with them.
|
| The interesting thing about 'monads' isn't bind and return.
| The interesting thing is the Control.Monad functions that
| build on them, and by making something that conforms to the
| monadic interface, you get to use them: https://hackage.haske
| ll.org/package/base-4.15.0.0/docs/Contr...
|
| Just like the special thing about iterators isn't that you
| can get the "next" thing. That's not impressive on its own.
| The magic is in the tools you get that can generically use
| and manipulate iterators if they conform to a particular
| interface.
| lalaithion wrote:
| Example of a time when the Control.Monad functions were
| useful to me:
| https://lalaithion.xyz/posts/practical_monad.html
| endgame wrote:
| Exactly this. The power of an abstraction is roughly the
| product of two metrics:
|
| 1. How many things "fit" this abstraction?
|
| 2. What does this abstraction let me say?
|
| The abstraction "Monad" has many inhabitants, and the
| Control.Monad module shows that it lets you say many things
| -> it is a very useful abstraction. The abstraction
| "Applicative" has many more inhabitants, but lets you say
| fewer things -> it is still a useful abstraction.
| AzzieElbab wrote:
| agreed but still impressed by authors effort
| tessierashpool wrote:
| yep. as the OP said: _There is a reason why most monad
| tutorials use Haskell: without syntactic support, monads are
| pretty clunky to use._
|
| (You could probaby go further and say "without semantic
| support.")
| didibus wrote:
| I think Monad make more sense when you understand the context and
| its additional constraints as to why you'd want to use Monads.
|
| Imagine that you want to write code that is strictly pure, so
| that when evaluated, it doesn't do any side effect, it only
| returns a pure description of the computation it did.
|
| Here's a simple example: func announce-good-
| catch(fish) if(fish.size > 5) print "We
| found a big one! else print "That one won't
| even feed a cat, try again!"
|
| How can you make this function pure? It technically prints
| something, yet you want to run it and not have it print, but
| instead return you something that describes what branch it took
| and what effect it would have done had it been impure.
|
| You can do this: func announce-good-catch(fish)
| if(fish.size > 5) return [:print "We found a big
| one!] else return [:print "That one won't
| even feed a cat, try again!"]
|
| Now the function is pure, it doesn't print anything, it evaluates
| the branch to take based on input and return to you a description
| of what it should do.
|
| The problem is programming that way is hard, let's add another
| function: func get-fish(fish-id)
| return new Fish(db.query("select * from fish where id = " + fish-
| id)
|
| Again this isn't pure, so let's make it pure:
| func get-fish(fish-id) return [:new Fish [:query [:get
| :db] ["select * from fish where id = " + fish-id]]])
|
| Already this is getting pretty complicated, getting the DB is
| impure so return a description of it, querying the DB is impure,
| so return a description of it, Fish can't be created if we don't
| execute the side-effect, so take note that you want to create a
| Fish after those two side-effects were to actually happen.
|
| Now combine get-fish and announce-good-catch(fish)?
| announce-good-catch((get-fish 10))
|
| Well this works in the impure version, but this doesn't work for
| our pure versions because now the input to announce-good-catch
| isn't an actual Fish but instead it's this:
| [:new Fish [:query [:get :db]
| ["select * from fish where id = 10"]]]
|
| And so it's not going to work, you can't compose those pure
| functions as you'd normally would.
|
| Here is where Monads become really handy, they put all this
| behind an abstraction that lets you with added syntax sugar feel
| like your code is written in the impure style, yet under the hood
| it will convert things to a description of all side-effects and
| whatever pure behavior is pending the result of those. And it'll
| let you compose those descriptions together so that from the
| programmer point of view the code you write looks and feels
| almost the same as the impure version.
|
| This is the problem Monad solves.
|
| Now once you have that description, eventually you want it to
| actually run for real so that side-effects and all other
| described pending behavior gets executed. But what people
| realized here is you can kind of abuse this a bit, where you
| could choose to interpret the description in new ways, or
| manipulate the description prior to executing it. Basically
| whatever executes the description for real is in charge of how to
| do that and it could choose to change some of the assumptions
| like the evaluation order or the thread on which thing execute,
| etc.
|
| Thus once you have Monads, you can also use them to manipulate
| evaluation order and semantics.
|
| Now if you don't care about constraining yourself to pure
| functions, all this can seem like overkill, and it kind of is.
|
| Obviously, people will tell you it's a good idea to make
| everything pure, because you can more easily test things.
|
| And people will tell you, even if you don't care about the
| purity, how Monads let you manipulate evaluation order and
| semantics of the code can be cool and allow for some neat
| features, so maybe that's something you'd find useful, such as
| adding tracing throughout automatically, or automatically
| parallelizing things that don't depend on each other, etc.
|
| This is where some people say that Macros can often be used to
| similar effect. Because macros, a bit like Monads, operate on the
| description, macros on the code's AST itself, Monads on the
| captured Monadic composition. So a macro can also rewrite things
| to change their evaluation order or semantics.
|
| I wouldn't call myself an expert on all this yet, more of an
| enthusiast, but that's how I would contextualize it.
| Koshkin wrote:
| I think the List monad is easier to comprehend.
| didibus wrote:
| The List Monad is the same, if a function called with the
| same input can return one of many results, each time it is
| called it could return a different output. That means this
| function isn't pure, because a pure function maps the same
| input to the same output even when called repeatedly.
|
| The difference here is instead of returning a description of
| the impure things that should be done, you can make the
| function pure by returning the list of all possible options.
|
| That means instead of getting a different output non-
| deterministically for the same input each time the function
| is called, you now get the same output everytime, which is
| the list of all possible outputs for that input.
|
| So the context is the same, you want to constrain yourself to
| pure functions for some reason, so how do you make this pure?
| func favorite-color(person-name) if(person-
| name.startsWith("A") return randOf("blue",
| "green") else return randOf("yellow",
| "black")
|
| The trick is to have randOf return a list of all options such
| as randOf("blue", "green") =>
| ["blue", "green"]
|
| Now you have the same composition problem as before:
| func generate-favorite-color-string(favorite-color)
| return "My favorite color is: " + favorite-color("Abel")
| generate-favorite-color-string(favorite-color("Abel"))
|
| But this won't work again, so you need a Monad once more to
| help you with composing these pure versions of your impure
| functions so that: bind(favorite-
| color("Abel"), generate-favorite-color-string)
|
| Which says: compose generate-favorite-color-string with
| favorite-color using Monads
|
| And now what the Monad composition will do is it will
| automatically run generate-favorite-color-string for each
| element in the list returned by favorite-color and itself
| will return a list of all those possible results:
| ["My favorite color is: blue", "My favorite color
| is: green"]
|
| So the composition is also pure now.
| tfwwtn wrote:
| related: https://lispcast.com/are-monads-practical/
| Koshkin wrote:
| What is described looks to me like an interpreter for a Forth-
| like DSL in which the notion of the monad should find its
| "natural" expression. Still not sure if it can indeed be viewed
| as a practically useful extension of the base language
| (Clojure)...
| sesm wrote:
| I'm not an expert, but my understanding is:
|
| 1. You are writing a DSL that works with wrapped values.
|
| 2. In this DSL you can use arbitrary pure functions from your
| base language.
|
| 3. The value proposition is: those pure functions should not be
| aware of the fact that they are working with wrapped values,
| they are written as if they are computing regular non-wrapped
| values. Bind takes care of this.
|
| 4. Indeed the simplest way to implement this is some kind of
| interpreter. But you don't have to re-implement the entire
| language inside this interpreter, because of #3
| [deleted]
| sova wrote:
| Can someone give me a practical example of when one would want to
| use a monad? Typically I don't want side-effects to all live in
| one thing. It's cute but "we want to change the world," don't we?
| anchpop wrote:
| Two common examples from the Ocaml world are promises through
| the LWT library and options.
|
| If you just wanted to open a file, maybe you would write
| something like this:
| run(create_file("filename"))
|
| Here `create_file("filename")` is a pure value, no side
| effects. It's a value that represents the concept of creating
| `"filename"` (and maybe returning a handle). When passed to
| `run`, that value is inspected and only then the actual action
| it corresponds to, creating a file, is executed.
|
| What if you want to do something with the file? That looks like
| this: let create_then_delete =
| and_then(open_file("filename"), (handle) ->
| delete_file(handle)) run(create_then_delete)
|
| `create_then_delete` represents the concept of opening a file
| and giving it to `(handle) -> delete_file(handle)`. If you want
| to actually do that, you pass `create_then_delete` to `run`.
|
| Functions with a type signature similar to `and_then` turn out
| to be pretty common, so it's nice to have a way to talk about
| them. That's basically what monads are for. In OCaml, all
| functions that have a similar type signature can use the new
| "let-monadic syntax", which basically lets you rewrite
| and_then(open_file("filename"), (handle) ->
| delete_file(handle))
|
| to let\* handle = open_file("filename");
| delete_file(handle)
|
| Which is a lot more readable if you ask me. Any type that has a
| function like `and_then` works with this syntax, and those
| types called monads.
| massung wrote:
| I believe that most descriptions and blogs about monads
| completely miss the point.
|
| Monads are a tool. And yes, they can be used for things like
| having side effects in pure code or allowing for the chaining
| of functions, but those are just tangential benefits.
| Fundamentally, monads are just a way to categorize data, and
| the downstream results of that data. In the end, this gives us
| the ability to codify what we - as programmers - already know,
| and therefore depend on.
|
| Consider the following mad-lib:
|
| > Any value computed using the result of ____ must also - by
| definition - be the result of ____.
|
| Now, let's plug in a simple example with "asynchronous
| function":
|
| > Any value computed using the result of an asynchronous
| function must also - by definition - be the result of an
| asynchronous function.
|
| If I call a function call `httpGet` that returns
| `Future<Response>` and then I take the response and parse the
| HTML into a DOM object, I know - as a programmer - that the DOM
| object is of type `Future<DOM>`. I know this whether or not the
| programming language I'm using is dynamically typed, supports
| higher-kinded types, or not.
|
| Whether or not you prefer to live in Haskell land, Go land,
| Clojure land, or Flatland, monads are just a tool for you - the
| programmer - to categorize your data and codify (at compile
| time and/or runtime) what you already know.
| endgame wrote:
| Monads are not "for side effects". Monads are a programming
| pattern to facilitate code reuse. Any time you have a type
| constructor M (like List<_>, say) that supports the following
| operations:
|
| 1. pure :: T -> M<T>, and
|
| 2. Any one of the following three (you can derive any of these
| from any other)
|
| join :: M <M<T>> -> M<T>
|
| bind :: (M<A>, A -> M<B>) -> M<B> -- sometimes called "andThen"
|
| pipe :: (A -> M<B>, B -> M<C>) -> (A -> M<C>)
|
| If they interact with each other in a "sensible" way (the
| "monad laws"), you have a Monad.
|
| ---
|
| What things fit this abstraction? I think the most familiar one
| to mainstream programmers would be Promises. If you have a
| Promise<Promise<A>>, you can join them together to get a
| Promise<A>. If you have a Promise<A> that you need to do some
| further work on, using the bind operation with a function (a ->
| Promise<B>) lets you "bind a name" to the result of the first
| promise and return a new Promise<B>.
|
| If you'd like to be explicit about null-checks, then the
| Option<A> type lets you build up a computation without writing
| `if (foo != null)` everywhere. A variant, Result<E, A>, lets
| you stop on the first error. Tracking explicit error
| information in this way has been invaluable when taming a large
| ruby codebase - see the dry-monads project.
|
| Any time you want to describe interactions with a system, this
| abstraction gives you something like the "command design
| pattern" on steroids. I have used Transaction<A> types to
| representing database transactions, ensuring that arbitrary IO
| doesn't happen inside a transaction (you can't unsend an email
| if the transaction rolls back). A function of type
| ((Connection, Transaction<A>) -> IO<Result<E,A>>) executes the
| DB operations inside a DB transaction and either returns the
| transaction result or an error.
|
| None of this is "we want to change the world lol". I want to be
| able to notice the common elements between several type
| constructors, and be able to work on them using a common
| vocabulary, instead of reinventing wheels.
| JoelMcCracken wrote:
| IMO, the best articles I know of on monads, which take two very
| different approaches:
|
| - http://www.jerf.org/iri/post/2958
|
| - https://philipnilsson.github.io/Badness10k/escaping-hell-wit...
| sesm wrote:
| See also https://github.com/clojure/algo.monads - part of Clojure
| standard library which has definitions of common monads and links
| to tutorials
___________________________________________________________________
(page generated 2021-10-05 23:01 UTC)