Remix.run Logo
kriops 4 days ago

Monads are well-defined, though. Monoids in the category of endofunctors, anyone?

The problem is when implementations aren’t actually monads at all. The same goes for other functional concepts. I wrote a blog about Java’s Optional::map here: https://kristofferopsahl.com/javas-optional-has-a-problem/

It’s the same kind of problem, where naming signals something the implementation is not.

(Am I allowed to link my own blog btw?)

whstl 4 days ago | parent | next [-]

> The problem is when implementations aren’t actually monads at all

Exactly, this was my point, it wasn't clear.

The original definition, and Haskell's implementation are good in itself. Monads in Haskell are not that difficult or too abstract.

It was Monad tutorials and partial implementations missed the mark, like in your example.

Myself, similarly, I've seen way too many Option<T> implementations in Typescript that are less safe than if (value !== null) {}, because they replace a static check with an exception in runtime.

lock1 4 days ago | parent | prev | next [-]

Unrelated with GP post: What's wrong with Java's Optional?

IIRC it doesn't fulfill monad axiom, but I don't think there's a huge problem with it. By the time you're using Option<>-like, I don't think you should use bare `null` at all in your project. Mixing Option<>-like and bare `null` sounds like playing with fire.

Also, if you're using Java 17+ (`record` in your example), you're probably better off writing your own Option<T> to support sum-type matching & product-type destructuring.

lmm 3 days ago | parent | next [-]

> IIRC it doesn't fulfill monad axiom, but I don't think there's a huge problem with it. By the time you're using Option<>-like, I don't think you should use bare `null` at all in your project. Mixing Option<>-like and bare `null` sounds like playing with fire.

That's completely backwards IME. The whole point of Option is to allow you to make precise distinctions, and not allowing null in it when null is allowed in regular variables is a recipe for disaster.

For example, the flagship use case of Optional is to make it possible to implement something like a safer Map#get(), where you can tell the difference between "value was not in the map" and "value was in the map, but null". A language that wanted to evolve positively could do something like: add Optional to the language, add Map#safeGet that returns Optional, deprecate Map#get, and then one chronic source of bugs would be gone from the language. (And yes, ideally no-one would ever put null in the map and you wouldn't have this problem in the first place - but people do, like it or not). Instead, Java introduced an Optional that you can't put nulls in, so you can't do this.

reverius42 3 days ago | parent [-]

I think they mean, not allowing null outside, and using the None variant of Option to represent a lack of something.

lmm 2 days ago | parent [-]

Removing null completely is a good end goal, but in order to get from there to here we need to migrate trillions of lines of code (and having an option type available is necessary to even get started), and if that option type can't accommodate all the values that are valid in the language and existing codebases (which currently includes null, like it or not) then we can't even get started.

kriops 4 days ago | parent | prev [-]

Optional::map returns an empty optional if the passed function returns null. This is incorrect and can be especially hurtful in intermediate operations. Allowing Optional::map to return an Optional<void> would have been correct.

Alternatively, just don't call it 'map'.

I agree implementing your Option<T> type is better. The problem is that people will use whatever is available in the standard library—I am not working in isolation.

ekidd 4 days ago | parent | prev [-]

Yes, monads are abstract, but the definition is also very precise. Specifically (using C++/Rust notation for parameterized types), if we have a type "M<T>", we also need:

    fn unit<T>(value: T) -> M<T>

    fn map<T1, T2, F>(input: M<T1>, f: F) -> M<T2>
    where F: Fn(T1) -> T2
...and finally the magic bit:

    fn flatten<T>(input: M<M<T>>) -> M<T>
This, in turn, allows defining what you really want:

    fn flatMap<T1, T2, F>(input: M<T1>, f: F) -> M<T2>
    where F: Fn(T1) -> M<T2>
...where the mapping creates an "extra" layer of M<...>, and then we flatten it away immediately.

(There are other rules than ones I listed above, but they tend to be easy to meet.)

Once you have flatMap, you can share one syntax for promises/futures, Rust-style Return and "?", the equivalent for "Option", and a few dozen other patterns.

Unfortunately, to really make this sing, you need to be able to write a type definition which includes all possible "M" types. Which Rust can't do. And it also really helps to be able to pick which version of a function to call based on the expected return type. Which Rust actually can do, but a lot of other low- and mid-level languages can't.

So monads have a very precise definition, and they appear in incomplete forms all over the place in modern languages (especially async/await). But it's hard to write the general version outside of languages like Haskell.

The main reason to know about monads in other languages is that if your design is about 90% of the way to being a monad, you should probably consider including the last 10% as well. JavaScript promises are almost monads, but they have a lot of weird edge cases that would go away if they included the last 10%. Of course, that might not always be possible (like in many Rust examples). But if you fall just barely short of real monads, you should at least know why you do.

(For example, Rust: "We can't have real monads because our trait system can't quite express higher-order types, and because ownership semantics mean our function types are frankly a mess.")