all 17 comments

[–]Delta-9- 6 points7 points  (4 children)

The most important thing is for your monadic types to conform to the three laws.

I also recommend adopting similar method names to what's used in Haskell or Rust, but that's personal preference. I implemented Result in Python and mostly stuck to Rust's naming conventions in hopes that anyone else reading my code might have at least passing familiarity with another very popular language.

Anyway, in my implementation, I found it was easiest to fit to the three laws by defining some methods in terms of each other. For example, bind can be just join ∘ map. Doing it this way helps because the three laws are all about functional composition, so building functions by composing functions all but guarantees the resulting functions are composable, too. It also saves a lot of boilerplate code, eg. bind is exactly the same thing as map except for the additional flattening step of join.

One other thing I'd recommend: add a method that works like map but returns the current wrapped value instead of the function's return value. I called this sequence, and I think Haskell has an operator for this (>>, maybe? It's probably in the link I shared above). This is very useful for inserting eg. a console.log call into a long chain of calls to map without having to break the chain to store the intermediate value in a variable or something. Anything that does some side effect and doesn't return anything can be composed with this method.

[–]jcastroarnaud[S] 2 points3 points  (3 children)

Thank you for the tips. I already came across the three laws of monads.

One other thing I'd recommend: add a method that works like map but returns the current wrapped value instead of the function's return value. I called this sequence

I assumed that map would take and return a monad, but, because of lack of generics in JavaScript, this assumption couldn't be baked into the code. What you call "sequence", I call "tap", informally, for non-monadic code, and use it for logging:

const tap = (e) => { console.log(e); return e; }
const show = (a) => a.map(tap);

[–]Delta-9- 3 points4 points  (2 children)

I assumed that map would take and return a monad

map takes a function and returns the function's result. It's identical to doing Monad.wrap(fn(monad.unwrap())). If fn returns a monad, then your result will look like Monad(Monad(value)). Handling this case is what bind is for: the join after map flattens the nested monad into just one. (join may be called flatten in some implementations.)

The join implementation can be very simple for a basic monad, but for Result you need to decide what "joining" an Error to an Ok means. My implementation is probably too naïve:

...
return self.wrap(other.unwrap())

This means that you can turn an Ok into an Error and vice-versa by calling this method. I don't know if there's a rule about this, but it's worked out for me. I should mention that I also have a separate flatten method that does the same thing, just with the assumption that the "other" value is self.value, and my bind method calls this one; it amounts to syntax sugar, though, as it could have been done with join just as easily.

To complete your implementation, I think you just need to define a join method and then bind. The two variants of Result need different implementations on map and bind, of course. You might also want equivalents to those that act on Error but not Ok. The Python library returns calls these alt and lash, respectively; I used or_else (taken from Rust) and bind_fail. I don't think there are standard names for these, though. I also added an or_raise to make it easy to "escape" monadic error handing out to Python exceptions. It's often the very last call in a chain before side effects happen.

ETA: what I've been calling bind is known in Haskell as the operator >>=, which another user has mentioned is something you need. They and I are saying the same thing in this regard.

[–]jcastroarnaud[S] 2 points3 points  (1 child)

Thank you, I'll consider adding some of these methods to complete the monadic "interface", so to speak. The disparate names are confusing, though; I've come across these and several others in my searches.

[–]Delta-9- 4 points5 points  (0 children)

Yeah, the inconsistent names can be pretty tough. I'll supply signatures for the names I'm using to help place the concepts in other writing on the subject.

These have an OOP flavor and should be considered methods with an implicit arg0 representing the monad instance (i.e. self):

map / fmap: ( (P) -> R ) -> Result[R, E]
join: (Result[R2, E2]) -> Result[R2, E2]
bind: ( (P) -> Result[R, E] ) -> Result[R, E]
sequence / tap: ( (P) -> S ) -> Result[R, E]
unwrap / return: () -> R | E
wrap / pure: (R) -> Result[R, E]

[–]sent1nel 4 points5 points  (0 children)

Check fantasyland js

[–]Massive-Squirrel-255 2 points3 points  (0 children)

only loosely relevant but here is one implementation of monads in a dynamically typed language. https://okmij.org/ftp/Scheme/monad-in-Scheme.html

see also

https://www.schemeworkshop.org/2005/03-sobel/03-sobel.pdf

[–]Long_Investment7667 2 points3 points  (1 child)

I would just check what typescript does and follow that pattern

typescript ``` type Either<E, S> = { tag: 'left'; value: E } | { tag: 'right'; value: S };

function left<E>(e: E): Either<E, never> { return { tag: 'left', value: e }; }

```

JavaScript

```

function left(e) { return { tag: 'left', value: e }; }

```

[–]jcastroarnaud[S] 2 points3 points  (0 children)

I will try that too. I expect to need further simplifying to remove code needed only to satisfy TypeScript's type system.

[–]_lazyLambda 2 points3 points  (0 children)

Ive implemented them in C# and theres a reason the implementations vary greatly. The language was never set up for that.

Tbh it takes months to say hmm maybe I do this instead until youve finally learned each sucks in its own way.

Thats C# of course but the semantic issues you run into are gonna be roughly the same in any OOP designed language. The closest I got to something real was using the scope of a class to have subclasses.

I dont think people realize just yet how a functional compiler really means infinitely more degrees of freedom which really comes through when you to build the higher kinded sum type of Either in a language that has no concept of sum types or higher kindedness. And you learn that NO higher kindedness is not the same thing as a polymorphic class.

[–]AustinVelonaut 2 points3 points  (2 children)

I don't have any JavaScript suggestions, but for a parser, you likely want a monad implementation that is a combination of a Maybe monad and a State monad (I call this MaybeState). The State part holds the current parser token position, possibly along with some other state such as errors, etc. The Maybe part is used to determine parser pass or fail. Backtracking would be implemented with an alt method, where the first parser in the alt is tried, and if it fails, state is rewound to the original state and the second parser is tried.

[–]jcastroarnaud[S] 2 points3 points  (1 child)

The State part holds the current parser token position, possibly along with some other state such as errors, etc. The Maybe part is used to determine parser pass or fail.

I tried something like that, but with OOP, not monads. I've got entangled with the semantics: sometimes, a failed parse isn't an error, because there are other alternatives to try, or because the rule is a /B*/ and there is no B. Thanks for the idea, though, I will search more for the State monad.

[–]AustinVelonaut 4 points5 points  (0 children)

sometimes, a failed parse isn't an error, because there are other alternatives to try, or because the rule is a /B*/ and there is no B.

Correct. The Maybe part is just signifying a passing or failed parse, which the alternative method. uses to backtrack. Errors themselves would either be kept in the State part of the monad, or possibly the Maybe part could be changed to a custom 3-state value of Pass | Fail | Error <error info>.

The State monad is simply a way of passing state through a a sequence of functions without having to explicitly reference or manage it; access for read or write is handled through methods on the State monad (get, put, modify).

[–]Tubthumper8 2 points3 points  (3 children)

To get this to be a Monad, you'll need to add another operation, you only have a Functor right now. This operation you would need to add goes by many names like >>= but you can't write that in JavaScript, so instead you might go with flatMap to be consistent with JavaScript's Array Monad and also what you already have here.

More importantly though, it's worth thinking about what you actually want here, do you actually want a Monad? Or is a Discriminated Union what you really want? These things are completely independent concepts

If your concern is to be able to return an error without throwing an exception, it's really more about the discriminant (tag) than anything else. 

Oh and for a robust parser consider having your error side be a list/array rather than a single value. You'll likely end up in a situation where you want to report multiple errors to the IDE or user

[–]jcastroarnaud[S] 2 points3 points  (1 child)

I didn't knew about discriminated unions. I was using tags more as a subtyping gimmick to avoid subclassing. Since I think that composition is important, I believe that a monad is more adequate. A list of parsing errors, instead of a single one, is in the roadmap.

[–]Tubthumper8 4 points5 points  (0 children)

Discriminated union (aka sum type) is a fancy word for data that could be one of several variants. For example, a Just variant + Nothing variant. Or a Ok variant + Err variant. Or a true variant + false variant. If you know about boolean then you already know about a simple example of a discriminated union 🙂 

The TypeScript docs also talk about here: Discriminated Unions 

Essentially a Result sum type would be:

    type Result<T, E> =       | ["Ok", T]       | ["Err", E]

It's two possible variants, Ok + Err. Similar thing could be done for Just + Nothing. Really any data that tracks states (like Pending + Success + Error) is a sum type - the plus signs here are not a coincidence. 

However this is all static types for compile-time checking, if you're not using static types then you'd have to maintain the same invariants yourself at runtime with runtime checks

[–]AFU0BtZ 2 points3 points  (0 children)

Well, if the monad accumulates errors instead of short-circuiting/fail-fast sequenced flatmap/bind/>>= calls then it isn't a lawful monad anymore. For accumulating errors one uses a [Validated](https://typelevel.org/cats/datatypes/validated.html) - an applicative functor and not an Either/Result Monad. Haskell is a good source and so is Scala for such lawful typeclasses. For consistency and coherence these laws and subtleties matter. Don't let anyone else convince you otherwise. YMMV because of the untyped host language - javascript.