> In languages that don’t have typeclasses, you need to take special steps to ensure that you dispatch to the proper variant of the monad.
You might want to take a look at F#, which has no typeclasses (yet) but does have excellent first-class support for monadic syntax via "computation expressions".
Here's something I whipped up to show an example of a ResultWriter monad using computation expressions in F#. I'm sure there are some slightly better interfaces than this, and I wouldn't mind a code review if anyone is feeling helpful!
I claim the visitor pattern isn't that weird. The `visit` function is the natural way to move from continuation-passing style to direct style, being of type signature `forall 'ret . (MyObject -> 'ret) -> 'ret`.
Explanations of monads are vastly more complicated than monads. Any Javascript programmer will recognise what .flatMap() is and does - you don't even need to know design patterns for that.
> This could work, but the fact that we are using global mutable state ...
What? Why would you consider using global state for this? I'd do it like this. Mutability without globals.
export function transform(ast) {
const queries = [];
walk(ast.root, queries);
return { ast, queries };
}
function walk(node, queries) {
// recursively called on child nodes
// can mutate node, and append to queries
}
> Surprise! This is exactly the writer monad! This whole post has been a monad tutorial in disguise!
Aha. That explains why it stopped making sense about 6 paragraphs ago.
You're right that you could create some local state and mutate that. That would be a better solution than some dumb global variable sitting somewhere. I considered touching on that, but this was already a long blog post!
I'd still be a little worried about your above solution though—I'd have to be sure of the state semantics in $LANGUAGE and I'd want to make sure the compiler wouldn't share copies of `queries` between invocations. (Maybe that's a weird thing to worry about, but having written compilers it's something that crosses my mind.) Also, I might also worry about that state leaking in other ways. Mutating Python's default arguments comes to mind.
Moreover, some languages (like Elixir in this post) don't even give you the option of having semi-protected local mutable state like the above. (Yes, I know about the process dictionary. No, I don't want to use that—my PR would be rejected immediately by all my coworkers if I did that.)
The point is, this feels like something that shouldn't necessitate having mutable state anywhere, so here's how to get to that nice, pure functional solution without sacrificing readability with tons of boilerplate.
The benefit of immutability is that values can never change "out from under you". But there seems to be a counter-point, which I haven't seen articulated much. In cases where object reference identities are meaningful, all references to an object are always current. Think video game characters running around in an arena interacting with each other.
I think immutability is a great tool for domains where it's helpful. That's the same position I have regarding mutability. I think the mutable Date object in javascript was a mistake. I also think it's hard to compete with the simplicity of a recursive `walk()` function that mutates the AST nodes and query queue in place.
Maybe I'll see the light when I manage to comprehend what a monad is.
A monad is just a fancy name for a design pattern in functional programing that manages side effects so you can write what looks like imperative code but that does some other stuff behind the scenes, eg, logging, async, exception handling.
They only make sense in a functional language that has support for “type wrappers”, eg, Result, Option, or custom made types like a ResultWriter.
The gist is you define a bind function that takes two arguments (a white lie), the first one of those “wrapped values” and the second a “function that takes an unwrapped value, does something, and returns a wrapped value”, and then the bind function “unwraps the value from the first arg and calls the second arg with that unwrapped value”.
This lets you chain together these “functions that take a value, does something, and then returns a wrapped value” and if you can define an infix operator it makes for for legible code that’s easy to read and modify. It looks even better in F#’s computation expression syntactic sugar.
Of course I am now going to try to explain this better with Yet Another Monad Tutorial to torture the internet with…
I should have issued a warning against trying to educate me. But I also know that explaining monads is like irresistible crack for those in the know. I'm half-convinced I have a learning disability that specifically affects my ability to comprehend monads. Years ago, I gave it a serious attempt. At this point, I think I'll be satisfied to never quite get it. I totally understand Option, (I think?) but I completely fail to understand what that has to do with side effects. I'm ok with this. Thanks for the effort anyway. I don't understand what ResultWriter is or could be. I don't understand what the white lie is.
The white lie is that in functional programming a function only takes a single argument and uses currying to mimic multiple arguments.
Like this in JS:
let f = (x) => (y) => x * y;
let p = f(2)(3);
Is this in F#:
let f x y = x * y
let p = f 2 3
A ResultWriter is just a wrapper type that takes a tuple of a Result and a list of log messages.
The side effects are you can write imperative code that works with normal values but does exception handling and log messaging behind the scenes and without it cluttering the task at hand.
So you write some functions that return either Success wrapping a value with some good news log message or an Error with some bad news log message. Then with a bind function or especially with a computation expression in F# you can use those functions without dealing with the exceptions and log messages until after you’ve completed all of the steps.
Unless you’re using a language that supports this kind of monad design pattern it won’t really make any sense!
> I think the mutable Date object in javascript was a mistake.
I'm not much of a JS guy, but isn't mutable objects the norm, not the exception? Over in Java land we have the mutable ArrayList and the mutable HashSet etc.
> Maybe I'll see the light when I manage to comprehend what a monad is.
That's not going to happen.
First you need to see a problem (e.g. a mutable object being a mistake.)
Then you need to see a range of solutions.
Then the best solution needs to be a particular Thing.
Then that Thing needs to resemble other different Things which solve other problems.
If those different solutions resemble one another enough, you can put them under a common interface Monad.
But if there is no problem with a mutable AST object or a mutable Date object, then you won't even consider the solutions. A monad won't solve the problem you don't have.
Mutable and immutable objects are both common. In Java, ArrayList is mutable, but String is not. If you concatenate strings, it makes a new string. It never changes the existing one. This maps pretty well to peoples' intuitions when using strings. If you have a reference to a string, its' contents can never change. You can assign a new string to your variable, but that's different. In JS, the contents of a Date object can change. This is widely regarded to be a mistake, and I agree with that.
The label 'global mutable state' has a slightly broader reach in FP. Just because you don't see a 'static int' hanging out in the space between methods doesn't mean that you haven't given the whole world mutable access into your state.
walk can mutate node. transform can invoke walk. transform is public.
> Whenever we call our transform function, we would have to ensure that we clear out the old list of accumulated information.
This is why I presumed this post is using the popular understanding of the term.
Anyway, nodes are only public if the calling code makes them public by leaking a reference or something.
> Don’t forget about all the other problems that global mutable state brings. There must be a better way.
When this blog says "global mutable state", then it must be referring to what's commonly understood to be "mutable state". It seems to be an article of faith that there must be a better way. To my sensibility, that is the best way. The argument seems to rest on some sleight-of-hand regarding "global".
Remember 20 years ago, when getters and setters were all the rage?
Reaching in to access variables was bad, but if you used a getter instead, then that was somehow good because you were doing encapsulation?
IDEs gained the ability to generate your getters and setters for you, C# had properties which streamlined things and made Java look boilerplatey in comparison, and Java got Lombok.
But it was all bullshit. If making fields private is 'locking the door', then adding getters and setters is 'leaving the key on top of the doormat'.
Show me a logic bug caused by a global variable, I'll slap some getters and setters onto it, give it back to you, and it will still have the same logic bug.
What's my point?
It's not that FP has some competing definition of 'global variables' that somehow makes FP look good and imperative look bad, it's that FP actually followed the idea through to its logical conclusion and generalised it as 'shared mutable state', rather than slapping "encapsulation" on it and calling it a day.
> Anyway, nodes are only public if the calling code makes them public by leaking a reference or something.
This is true. However people commonly leak things by putting 'export' in front of them. Otherwise how can callers access them?
I do remember. I never really bought into the dogma about property accessors. Sometimes they are good. But mutable object fields might be fine too. In fact, I even think goto is appropriate in some circumstances.
I guess the modern conception of FP says 'all values and state are best modeled immutably'. Whereas I tend to think it's only some of them.
Sounds like some Dunning-Kruger inability to grasp how to solve the problem working with the tools rather than imposing their ideas that don't fit. If you need a "global mutable variable" in BEAM land, then you could just use a genserver or ETS. It depends if the solution needs to be a cluster-aware singleton or not.
In the case of business logic and Elixir, this is where you may need to write your own DSL to model domain logic (DDD). It makes it makes things more flexible and more maintainable than piling together mud and hoping it won't fall over and that someone else in the future will be able to reason about it and maintain it.
I say "global mutable variable" to introduce the problem, followed by "global mutable state" in the summary of the first solution. The evil isn't a variable, it's just global mutable state in general. In cases where you need state, ETS and GenServers are good solutions—we agree there. But are you suggesting that the right way to solve this problem would be to spin up a GenServer every time you need to run what should be a pure function? I'd almost rather use the process dictionary!
What I'm saying is, this problem shouldn't need state to solve it, therefore any introduction of state is a hack that will be a headache to maintain, test, and reason about.
> In the case of business logic and Elixir, this is where you may need to write your own DSL to model domain logic (DDD). It makes it makes things more flexible and more maintainable than piling together mud and hoping it won't fall over and that someone else in the future will be able to reason about it and maintain it.
I don't know what you're referring to here. What problem would a DSL solve (and how?), and what do you consider to be the mud that's being piled on?
How else are you going to get global atomic, possibly highly-available share nothing mutable state? Magic? GenServers aren't difficult. Use the patterns rather than wincing or complaining about the lack of nonexistent wishes if you're not going to build a simpler solution.
Business rules are often represented as an ordering of constraints and triggers in models when using DDD & MVC. One example in another language would be trailblazer.[0]
Mud is when gobs of fragile, low-level of code is slapped together just to solve today's problem where everything is a special case without testability, architecture, standardization, or concern for maintenance or extensibility.
You're right. GenServers are wonderful and simple. I'm just saying this problem doesn't need them.
> Use the patterns…
I am. Monads are just another pattern. You know the `with {:ok, ...} <- ... else ... end` pattern in Elixir? That's another monad!
I show one concrete example here in my post that monads cover well. I've come across other scenarios in industry where monads can cut down on boilerplate better than anything else. Would another solution work? Sure—it's a Turing-complete language. Would it be as clean? Sometimes not.
> Business rules, mud
I don't need definitions—I understand those concepts just fine. I'm just trying to understand why you think it's relevant to my post. What's the specific "mud" here?
If you like monads, maybe you should look into Haskell or Idris.
Please let me know when you write an interesting post that solves a class of problems rather than getting excited over something so trivial. Perhaps I'm jaded. Good luck to you.
> A functional programmer is one who calls their variables `f` and `x`, and their design patterns "zygohistomorphic prepromorphism".