7 ms·
Personally, I think monads are hard to understand in Haskell mostly due to its quite complex and comparatively ad hoc type system. Let's remove all the complex
by triska 5y ago
Personally, I think monads are hard to understand in Haskell mostly due to its quite complex and comparatively ad hoc type system.
Let's remove all the complexities that are introduced by the static type system for a moment, and focus on the essence. To illustrate the key point, let's use a dynamically typed logic programming language such as Prolog, where we think in terms of relations instead of functions.
Suppose we have a predicate definition of this shape:
pred(S0, S) :-
goal_1(S0, S1),
goal_2(S1, S2),
goal_3(S2, S3),
...,
goal_N(SN1, S).
In this case, the arguments clearly follow a pattern: They are "threaded through", starting from S0, and ending with S, in a sequence of the form S0 → S1 → S2 → ⋯ → S. This shape of argument passing is common, for example if you want to describe successive "modifications" of a data structure in the form of relations between an initial state S0 of the data, several intermediary states (S1, S2, ...), and a final state S. Or successive applications of side-effects: Think of sending packets over a network, or writing a sequence of strings to a file, where each such relation affects the state of a socket or other entity. Or when describing strings, where the string becomes successively "more known" in the form of further and further instantiation of a list of characters.
You can think of monads as mechanism to make this cumbersome explicit passing of arguments implicit. For example, since the arguments that are passed around follow such a clear pattern, we could write the example above equivalently as:
pred -->
goal_1,
goal_2,
goal_3,
...,
goal_N.
and let the compiler do the necessary transformation for us. In fact, that's exactly what also happens in Prolog, using definite clause grammars (DCGs), which you can think of as a mechanism to implicitly describe such a passing of arguments, so that you do not have to write them yourself.
- throwaway17_17 5y agoCan you expand on what you mean by calling Haskell’s type system ‘ad hoc’? I was under the impression that ‘ad hoc’ was basically the opposite of formalized/well-founded. That doesn’t sound like an accurate description of Haskell’s type system in my understanding.
- triska 5y agoFor example, aptly named ad hoc polymorphism and overloading violates referential transparency: ( (7^7^7`mod`5`mod`2)==1, [False,True]!!(7^7^7`mod`5`mod`2) ) This yields: (True,False) suggesting that the same arithmetic expression is both 1 and 0. Yet, the ability to plug in expressions and reason algebraically about their values was frequently advertised as a core property of Haskell. This example shows that we cannot do this in general, we must also take into account the types, which in this case seem to be defined in a rather "ad hoc" way, by reflecting low-level implementation details such as the range of admissible integers in the type system. In GHC, there is a dedicated flag to spot such cases (-fwarn-type-defaults). In general, the guarentees that the type system actually gives and also the ways to specify them appear somewhat unfinished and are also quite hard to understand, often necessitating semantic restrictions and syntactic extensions. For further examples, see for instance: https://stackoverflow.com/questions/27019906/type-inference-interferes-with-referential-transparency https://stackoverflow.com/questions/27019906/type-inference-... https://stackoverflow.com/questions/14865734/referential-transparency-with-polymorphism-in-haskell https://stackoverflow.com/questions/14865734/referential-tra...
- pwm 5y agoFor readers wondering why this is, there are a few things at play here. I preface my explanation by saying that the benefit of type classes vastly outweigh issues like the above example. Also if I put the above example in my work project I immediately get a warning about the exact issue, so it's not like it is a foot gun or anything like that. Anyhow: 0. Integer is arbitrary precision while Int is bounded, machine dependent (eg. 32 or 64 bit). They are both instances of the Num type class as well as the Integral type class as we'll see later. 1. Numbers without explicit type signatures are overloaded (aka. constraint polymorphic): λ> :t 1 1 :: Num p => p where p can be any type that has a Num constraint, like Integer or Int. 2. As per https://www.haskell.org/onlinereport/decls.html#sect4.3.4 https://www.haskell.org/onlinereport/decls.html#sect4.3.4 we have default (Integer, Double) as concrete types for numbers to default to in expressions without explicit type signatures to guide inference. 3. The type of the list index operator is: λ> :t (!!) (!!) :: [a] -> Int -> a where the index is a concrete Int type. Right, so in the above example if we check the type of 7^7^7`mod`5`mod`2 λ> :t 7^7^7`mod`5`mod`2 (7^7^7`mod`5`mod`2) :: Integral a => a it is still overloaded (Integral), ie. can be either Integer or Int. Now in the first case there's nothing to concretise the type thus we are defaulting to Integer as per the defaulting rule. In the second case the usage of (!!) concretise the type to an Int. As 7^7^7 is big it does not fit in an Int (overflows). Compare: λ> 7^7^7`mod`5 :: Integer 3 λ> 7^7^7`mod`5 :: Int 2 The mystery is now solved. Side note: if we do default () to prevent GHC defaulting we'll get a type error and we will be forced to specific a type. We can also say: λ> ( (7^7^7`mod`5`mod`2)==1, [False,True]!!fromInteger((7^7^7`mod`5`mod`2)) ) (True,True)
- AlexanderDhoore 5y ago"Let's forget about Haskell for a second. Look at this Prolog code." You people live in an ivory tower.
- deleted 5y ago[deleted]
- bollu 5y agoAnd what's the problem with living in an ivory tower, as someone who also lives in one? A monad is a mathematical idea. It has some learning curve, some inherent complexity that you can't magically solve. The post you're mocking attempts to draw a helpful analogy. Maybe it helps the original author who said they couldn't find an explanation of monads that they liked. Maybe it doesn't help. I don't see how name calling such conversation as "ivory tower" helps anyone
- neurotrace 5y agoThe comment was supposed to explain monads by common usage rather than as a mathematical notion. Prolog is not a commonly used language by us blue collar programmers. I feel I have a grasp on monads and yet that explanation meant little to me given my limited experience with Prolog.
- AnimalMuppet 5y ago> A monad is a mathmatical idea. Not here. A monad is a programming idea. It bears some similarity to a mathematical idea, but we aren't really doing category theory here. We're doing programming. The programming idea is simple: Anything that implements certain methods with certain semantics. That's it.
- fao_ 5y ago> And what's the problem with living in an ivory tower, as someone who also lives in one? The stated intent of the post is "to provide an understandable notion of Monads to people who do not get them", so it seems terribly ignorant of the audience to assume that they will know Prolog, a language known for incredibly arcane syntax that has mostly been unused except for a handful of circumstances, outside of the 1980s. The author wants to draw a parallel between Monads and Prolog's execution, however how many programmers understand Prolog? Of those programmers, how many haven't grasped monads? Even I, who has some Prolog knowledge, now has the mental and cognitive load of having to parse the Prolog that I haven't seen used properly in perhaps 6 years, on top of having to understand the analogy to monads, on top of having to understand the description of monads. If n-gate still existed, I would pay them to review this comment chain, personally.
- skissane 5y agoSo really monads are essentially an abbreviated syntax? Chaining functions, where (some of) the arguments are hidden from the language syntax and inserted by the compiler? How does that relate to “pipe operator” as in https://github.com/tc39/proposal-pipeline-operator https://github.com/tc39/proposal-pipeline-operator Are monads also linear types? I mean, if I have a monad which represents system IO state, each value of system state can only be consumed once in actual execution, I can’t split the IO state in two and print different outputs on each branch. But I can do that in a conditional expression because only one branch is ever actually realised. For IO state, data branching is only allowed when guarded by control branching.
- ParetoOptimal 5y ago> So really monads are essentially an abbreviated syntax? Chaining functions, where (some of) the arguments are hidden from the language syntax and inserted by the compiler? That's a part of it, for instance see: https://philipnilsson.github.io/Badness10k/posts/2017-05-07-escaping-hell-with-monads.html https://philipnilsson.github.io/Badness10k/posts/2017-05-07-... I recently wrote a note related to this that had a useful exchange: >> When you see a "do" block, where do you look to figure out what it's actually doing? > If you need to know, you look at what consumes the result. If it's abstract (say it's a top level binding `... -> m a` where the m is abstract) *you don't _need_ to know "what it is actually doing" - it should be correct for any choice of* `m`. The behavior, and the "laws" or perhaps general far-reaching properties, are the more important part of Monads I think. > How does that relate to “pipe operator” as in https://github.com/tc39/proposal-pipeline-operator https://github.com/tc39/proposal-pipeline-operator The items in the pipeline don't have a set of laws that let you know what's going on for sure without looking at the implementation I guess.
- Avshalom 5y agoa monad is a type that is basically a structure { X:value-that-we-care-about, Y:something-we-only-care-about-occasionally-if-at-all } and then a bunch of functions that let you pretend it's the value X, that's what the compiler is abbreviating for you, all the necessary wrapping/unwrapping or calling members. In the prolog example what we care about is the last/latest value and monads let us just shove the previous value(s) into a hole and forget about it, or at least ignore it until later. with error monads we don't always care if something failed (like a write) but we probably want to keep that information for later, so it just gets hidden and we can pretend we're just passing around a value (see https://fsharpforfunandprofit.com/rop/ https://fsharpforfunandprofit.com/rop/ ) I'm not particularly clear on how Haskell does the IO monad, but basically the hidden value is what lets the compiler keep track of what was done pre/post IO, allowing you to thread-the-whole-world through an IO call. "Do" uses the same trick as that prolog example, by threading previous calculations through a function call you can force Do Foo1 Foo2 Foo3 to be called in that order (important for a lazy language) so you don't have force it by nesting them Foo3 (Foo2(Foo1(X))) can get pretty ugly fast.