Live data from Hacker News

Pain Points of Haskell

dixonary.co.uk

211–220 of 322 posts

Re: Pain Points of Haskell

#211
post #184

Earlier quoted context omitted.

> I'm not sure I follow Imagine you have a context (the monad), which contains a value of type `a`... > "by hiding the extraction of the value contained in the monad... Means, you don't need to care about how we get the value out of the context, it will just be done for you. That's part of the monad's job. Each different type of monad may do it differently (a List will do it many times, an Option will do it zero or o…

This is a good explanation of how the Monad typeclass maps onto data functors, but I'm not sure that it translates well onto Control functors such as (-> r).

I was mostly just trying to clarify and unpack the GP's quite terse statement. Definitely not trying to write a monad tutorial ;)

Re: Pain Points of Haskell

#212

Earlier quoted context omitted.

A monad is an abstract interface for sequencing side effecting operations in a way that guarantees referential transparency. They do this by hiding the extraction of the value contained in the monad and passing that value to a user supplied lambda that takes a value of the type of the value contained in the monad and returning a new monad containing a different type. Thus, you cannot do step one after step two, becau…

What is "referential transparency"? > "by hiding the extraction of the value contained in the monad and passing that value to a user supplied lambda that takes a value of the type of the value contained in the monad and returning a new monad containing a different type" I'm not sure I follow. Is this like saying I have a function: let myFunc = (arg1: SomeType): NewType => {} And it takes SomeType, and returns NewType…

The reply by louthy is great, but it doesn't answer your first question:

> What is referential transparency?

A function reference that can be replaced at all call sites in a program with its body with all of the references to its arguments replaced with references to the call site arguments without the observable behavior of the program changing is referentially transparent.

In practice, it means that you can safely refactor lines of code into a function reference without worrying about breaking your program.

It is trivial to ensure that a function that does no side effects, like `addTwoNumbers(a, b)` is referentially transperent, and we do it all the time, and we have very little trouble reasoning about what functions like that will behave like at runtime, so we do it as much as possible anyway. We call it things like "single responsibility principle" and KISS, and it leads to more maintainable programs.

However, it isn't as easy to do with a function like `printStringLine(aMessage)` or `divideTwoNumbers(a, b)`. Obviously, if you move your print statements and the state of a program changes, you'll get different output depending on where you moved them to. And the behavior of divideTwoNumbers when the dividend is 0 is problematic. As is making a network call --> depending on when it is called, you might get a different answer. All of those things are things the programmer has little to no control over.

The monad interface provides a way to make those things referentially transparent, often storing the transformations made in a sequence of bind calls in a data structure, then interpreting that data structure via a method called `unsafeRun`. Since the return of bind is now a data structure containing the transformations to be applied, it is possible to test that the network calls, console output, and error raising and throwing logic outputs in the correct order by substituting a "logging" implementation of the monad in tests rather than an "executing" monad in tests. Then you can test the "executing" monad by sequencing two operations that produce output and checking that that output matches your expectations, rather than checking that all of the network calls in your application actually work at test time.

Additionally, since we can usually unit test our non-side-effecting functions easily, using a monad method is a type-safe, code-level indicator in code that "trouble out of our control at runtime may happen HERE." So if you have trouble, and you are making a network call in your bind/flatMap, you know where to look: in your NetworkCallMonad implementation.

Referential transparency allows us to reason about a program's behavior by just looking at the body of the function, rather than anything else around it. It's the ultimate form of encapsulation, and has the same benefits. So, now that we have a way to define referentially transparent side effects that encapsulate problems that can only happen when the stars align incorrectly, we can reason locally and test locally around all the things in the program, which means the program is simpler to reason about.

There are some consequences, though. ANY 2 Monads won't compose. List>.bind((i) => i + 1) won't compile, or work. You need a MonadListTransformer that knows which order the monads are composed (Does Option contain lists, or does the list contain options?) in order to do the extraction and flattening in the correct order. Of course, this adds runtime execution overhead, and in the case of two nested monads is not too difficult to acheive.

But often, when you encode a program's effects into monads, you end up with monstrosities: >>>>. That can get to be a mouthful, and our simple little bind definition is now several layers deep. All that means is that you need to add some methods to your Monad interface, and give them new names, so that you have a type that is a Monad and a IO and a Monoid and a ApplicativeError. Those may sound like gobbledygook names, but they are the names chosen by mathemeticians and the FP community, so to Google them you have to use the names.

Monads, and `TypeClasses` are not the only way to achieve RT programs, but they are pretty widespread, well-defined, well-tested, and well-documented interfaces that will work. In languages without higher-kinded-generics (Generics that can hold other generics), you can simulate them in any language with generics using `Box` and `Unbox` -- typescript does this in its fp lib, for example -- so that you can still get the code reuse out of defining simple instances that can be derived from things higher in the dependency tree.

Anyway, they have value for simplifying programs, but all of their value is derived by maintaining referential transparency throughout the entire codebase as much as possible, and by staying within a context unless there is a safe way to exit the monadic context (This is called a `Comonad` -- at allows you to get a value out of a Monad without risking behavioral changes in your application, and not all `Monads` have a corresponding `Comonad`) until the very last line of your main file. Organizing many custom monadic effects is a common concern, and a really good solution for that is to use a single monadic type that can handle all of your effect needs -- like the IO monad in haskell -- and using the good `ol interpreter pattern to implement methods that delegate to the one base monad to do their effectful work. This is called tagless final style.

One consequence is that you do a lot of wrapping and unwrapping in your code with monads. It gets tedious to call the same constructor over and over again. Haskell, and other languages with extension methods, make this easier by automatically lifting call sites into the currently used Monad context type implicitly. Otherwise, you have to call new Monad(new Monad(new MyNetworkCall("myApiAddress")).map((networkCaller) => networkCaller.callGet(1))).flatMap((result) => new MyNetworkCall("myOtherApiAddress").map((netWorkCaller) => networkCaller.post(result.user)))

a lot, which is safe, but tedious to write and read. If you have to do this because of language limitations, refactor and extract as much as possible so that your code is readable AND safe.

Obviously, if your language or a library in your language doesn't define the standard typeclasses, of which Monad is only one interface, defining them and using them can be a real pain. Using them is kind of like using any framework --> trivial programs that are small should not use monads unless screwing up at runtime is VERY expensive. A lot of business applications are neither small nor trivial, and screwing up can have dire consequences, so a lot of programs can benefit from this level of encapsulation. It's just another tool, a proven (in the mathematical sense) tool, in your toolbox that can help code quality. Referential transparency isn't a magic silver bullet or a panacea. You still have to think.

> I'm not sure I follow. Is this like saying I have a function: > let myFunc = (arg1: SomeType): NewType => {}

Not quite.

    let myFunc = (arg1: SomeType): SomeType => {}
The `SomeType` wrapping the `A` is returned as a new instance of `SomeType` that now contains a `B` and not an `A`. The argument `arg1` is not mutated. The `A` it holds is extracted, transformed through user code into a `B`, and then placed back within a new instance of `SomeType`.

This obviously happens a lot in any language with generics. Like, all the time. And, every generic will implement the way it does the extraction differently, but the transformation always gets applied to the extracted value(s) in the same way, by calling the user function on the extracted value(s), then ensuring that if multiple instances are created (because multiple values were extracted) that only one instance of the generic that contains all the transformed values is returned.

Here's a simple example of it in use:

    let getCharsFromStrings(strings: List): List = bind(strings)((s: String) => s.map((c:Char) => c)))
    getCharsFromStrings(List("one","two")) // outputs List('o','n','e','t','w','o')
Since each string is broken into a list of its characters, you might have expected the return to be a list of two lists of characters. A Monad does the flattening for you. In fact, in some languages it is called `flatMap`, because it applies you function to each element that is extracted and flattens the nested structure by one level, turning your list of lists of chars into a list of chars via concatenation.

Re: Pain Points of Haskell

#213

This is nice, and yet, not much different from what the Haskell community was dealing with 5 years ago. They are also mostly programming concerns, as opposed to engineering concerns. How do you deal with simple things like exceptions and string interpolation? There are guides trying to explain monads, but no straightforward answer for dealing with these sorts of things, with building separate libraries / packages, se…

As a PhD student, I don't have enough experience working with Haskell in an industrial context to comment on the engineering concerns. I believe you when you say that these things are all problems. However, I'm aware that at least some solutions are underway. For example, the shake library [1] is a nice way to set up competent, reproducible build systems.

[1] https://hackage.haskell.org/package/shake

Re: Pain Points of Haskell

#214
post #211

Earlier quoted context omitted.

This is a good explanation of how the Monad typeclass maps onto data functors, but I'm not sure that it translates well onto Control functors such as (-> r).

I was mostly just trying to clarify and unpack the GP's quite terse statement. Definitely not trying to write a monad tutorial ;)

Oh, sure! In that case, mission accomplished with flying colours :)

Re: Pain Points of Haskell

#215

Earlier quoted context omitted.

I promise, you don't need to be a mathematician to use Haskell. If you've written shell scripts before, you understand how Haskell functions calls are written. If you've used `forEach()` in imperative languages then you understand functors. These two things alone are half of the groundwork you need to read basic Haskell code! Of course, the fundamental underpinnings of the language will not appear just from a surface…

> If you've used `forEach()` in imperative languages then you understand functors. You understand a single (or a class of) functor but I don't think it means you understand functors in general. You also definitely don't understand how functors fit into Haskell's type hierarchy — its relation to monoids, monads, etc.

Absolutely.

I'm only saying in relation to the idea of "reading Haskell code", it is possible to understand how they work (at the shallowest level) by analogy to `forEach` and to shell syntax.

Shell syntax also doesn't explain important constructs like currying, partial application, etc. But it's good enough that someone can read "boring" haskell code without being intimidated and turned off.

Re: Pain Points of Haskell

#216
post #202

One additional pain point for me is the number of symbolic operators. It’s hard to search for what some of them do, and even harder to have a conversation with a coworker when half your code is things like or >>=.

Those two operators are very common, and they're typically called "fmap" and "bind", respectively.

Re: Pain Points of Haskell

#217

It strikes me that I need to be a mathematician to use Haskell. Especially when someone like Rob Pike makes claims that "I cannot read the syntax of Haskell and understand it."

Hello. I failed high school maths.

I write Haskell every day, professionally.

Re: Pain Points of Haskell

#218

Earlier quoted context omitted.

I promise, you don't need to be a mathematician to use Haskell. If you've written shell scripts before, you understand how Haskell functions calls are written. If you've used `forEach()` in imperative languages then you understand functors. These two things alone are half of the groundwork you need to read basic Haskell code! Of course, the fundamental underpinnings of the language will not appear just from a surface…

> If you've used `forEach()` in imperative languages then you understand functors. You understand a single (or a class of) functor but I don't think it means you understand functors in general. You also definitely don't understand how functors fit into Haskell's type hierarchy — its relation to monoids, monads, etc.

You don't need to understand the functor of functions in order to be productive in Haskell and use functors generally.

Re: Pain Points of Haskell

#219
post #125

Earlier quoted context omitted.

> Haskell really is a "secret weapon" and people who use it for production tend to love it That's exactly my question. I'd be really interested in hearing about domains where Haskell is a secret weapon and how it compares to ML family languages and dependently-typed ones.

I did a bunch of professional Haskell work in a prior life[1]. Most of its 'secret weapon' status springs from the way Haskell lets you control side effects. We had a fantastic unit testing harness[2]. With effect tracking, you can arrange for your harness to put precise fences around nondeterministic code. You don't need to rely on experience and discipline to ensure that tests are reliable or fast. The type system…

Of all the things I like in haskell (having only used it a as a hobbyist), refactoring tops the list. It's a lot less scary to refactor when the compiler tells you so much about which parts you missed.

Re: Pain Points of Haskell

#220

Earlier quoted context omitted.

I fully grok monads. I’ve done enough reading and usage of them to understand them. They are not a useful abstraction for programming. They’re mathematically correct, but that’s not the same as what an industrial engineer needs. The big warning sign is monad transformers. Alone, monads are totally fine, but the issue is that you rarely want one . So you end up with this unwieldy tower of transformers that would make…

>They are not a useful abstraction for programming. They’re mathematically correct, but that’s not the same as what an industrial engineer needs. Pray tell then, how do you sequentially compose functions operating on values wrapped in an ADT like Maybe or Either?

You don't. You just do it the dumb old simple way, with some boilerplate here, and some boilerplate there. It may not be elegant, but it's not a blocker. Haskell needs more than this, if it's to find wider adoption.
Post reply on HN