
A Pythonista's Review of Haskell - yingw787
https://bytes.yingw787.com/posts/2020/01/30/a_review_of_haskell/
======
hopia
Making it to the end of an almost 2000 page Haskell book in only 3 months is
quite the feat.

While quite confusing to many, myself included, I would've loved to hear more
about the types vs terms distinction, and overall about what's possible with
type level programming. That's something fairly unique among programming
languages after all.

I'd certainly be interested in reading more of the author's thoughts if he
continues down the path of using Haskell to solve problems.

~~~
bojo
I would highly suggest
[https://thinkingwithtypes.com/](https://thinkingwithtypes.com/) \- it's not a
2000 page monster, and specifically addresses what you are interested in.

~~~
hopia
I've been reading that book occasionally for months but honestly, many parts
are somewhat too advanced for me for now. I've sort of backed off from type
level programming for now to maintain productivity with building actual
things. Still, I'd like to read about it from someone coming from imperative
languages.

------
nurettin
> IMHO, the solution to high cognitive load, given a high enough reward, is
> just persistent, methodical execution towards learning.

It will never converge towards being easy to handle. The load doesn't come
from being inexperienced, it comes from keeping the big picture in your head
(all your types and their interactions) instead of iterative thinking.

~~~
whateveracct
this isn't true - at least not in my years of professional experience

I barely keep anything in my head most of the time when programming in
haskell. It's kind of related to the "emphasis on structure" point from the
article. All of a type's "interactions" are just functions. There aren't any
second-order things to consider due to referential transparency.

~~~
hopia
I agree, type signatures & inference and referential transparency offload the
cognitive load from your head to the compiler/IDE.

Functions that actually work on the data structures are generally guaranteed
to be able to work on the said data. This way you don't need to constantly be
mindful of what the data looks like, the compiler does that for you.

~~~
anchpop
This exactly. Although the effect is much greater if you turn on some complier
warnings (like -fwarn-incomplete-patterns). The emphasis on coming up with
types that contain the guarantees you need makes it so once it compiles, you
don't have to remember what guarantees you need to have. Although doing that
is not nearly as convenient in Haskell as it could be (types often don't
compose well, and you need to add strictness annotations here and there to
prevent `undefined`), it is actually possible which is a big step up from
languages where you can't even try like C or Python.

------
pjdorrell
In Python

type(type) is type

evaluates to True. And that is not permitted in mathematical type theory that
the Curry-Howard correspondence applies to.

~~~
pdexter
Although it itself is deprecated, the TypeInType extension does exist.

[https://www.seas.upenn.edu/~sweirich/papers/fckinds.pdf](https://www.seas.upenn.edu/~sweirich/papers/fckinds.pdf)

[https://www.reddit.com/r/haskell/comments/4180k3/what_is_typ...](https://www.reddit.com/r/haskell/comments/4180k3/what_is_typeintype/)

[https://www.reddit.com/r/haskell/comments/4180k3/what_is_typ...](https://www.reddit.com/r/haskell/comments/4180k3/what_is_typeintype/cz0onpy/)

~~~
matt-noonan
And the only reason the language pragma is deprecated is because TypeInType is
now just how things are, by default, with no opt-out.

------
crimsonalucard
>Haskell has no null definition of type Char, at least according to this Stack
Overflow answer. Python doesn't distinguish between Char and String types with
single and double quotes, so empty char is empty string is an empty list. It
seems weird to me that for all the typing wealth Haskell provides, this base
case doesn't exist, though I personally don't know what it is.

An empty string is equivalent to an empty list []

If you want a null use the maybe monad.

~~~
inimino
As anyone who has used C will recall, a char is just a small integer. And just
the same in Haskell, a char is exactly one character. Once you understand
that, this confusion makes about as much sense as expecting an integer to be
"empty".

I suspect this kind of question would be better asked on the Haskell IRC
channel or similar rather than trawling stack overflow for insights. If
someone has the relevant missing insight they can quickly point it out and
save a lot of confusion.

~~~
crimsonalucard
it does make sense. 1/0 = undefined aka null. The thing with haskell is that
the concept of null is typed as part of a sum type meaning it will be handled
with no runtime errors.

~~~
pdexter

        *** Exception: Prelude.head: empty list

~~~
giornogiovanna
Yeah, I don't know why Prelude.head doesn't return a Maybe. Anyway, the point
stands.

• In Java forgetting to check for null will crash your program.

• In C, forgetting to check for NULL will lead to undefined behavior.

• In Haskell, you either have to (a) check for Nothing, or (b) explicitly say
"crash the program if this is Nothing" by using a function like fromJust or
head, which internally does (a). This is always an improvement over C, and
often an improvement over Java.

Side note: I like Rust's convention of consistently naming these might-crash-
your-program methods "unwrap" and "expect".

~~~
pdexter
Yeah I think rust has better defaults here.

~~~
crimsonalucard
I get the choice. It's unintuitive for basic types to be wrapped in a maybe
mondad. People expect addition, subtraction and division to return numbers. To
have division be the only operation to return an optional is a bit off.

I get the choice of why it wasn't done with haskell even though I disagree
with it.

------
sjakobi
FYI OP: I posted your review on r/haskell where it got several comments:
[https://www.reddit.com/r/haskell/comments/ewjnf4/a_pythonist...](https://www.reddit.com/r/haskell/comments/ewjnf4/a_pythonistas_review_of_haskell/)

~~~
yingw787
Thank you very much for sharing it there! The more the merrier :)

------
unnouinceput
Quote: "I don't think you can do anything like this with Python or any other
practitioner's language that I'm familiar with"

So, then you're not familiar with C/C++, Java, C#, Pascal/Delphi, Ada - to say
the least.

~~~
giornogiovanna
Yes, of course you can encode it in any language you want, but the point is
that ML-ish languages make it extremely natural to think like this, and Java
makes it impossibly painful. Haskell makes traditional imperative algorithms
painful, though, which is arguably a greater loss.

~~~
sweeneyrod
> ML-ish languages make it extremely natural to think like this

You say that, but actually fmap doesn't exist in OCaml (or I assume any other
common MLs). Of course you can embed a language and type system within it that
does have it ([https://blog.janestreet.com/generic-mapping-and-folding-
in-o...](https://blog.janestreet.com/generic-mapping-and-folding-in-ocaml/))
but that's different.

~~~
giornogiovanna
Using List.map and Option.map is more verbose and less general, but it's still
most of the way there.

OCaml still has currying, algebraic data types, "implicit" returns, tail call
optimization, etc., and it encourages recursion instead of loops wherever
applicable.

------
zzo38computer
They mention indentation, but you don't have to use their indentation and can
use braces and semicolons instead if you prefer, which is what I prefer, too.
So, I think the indentation isn't so much of the problem.

------
intrepidhero
"A functor instance lifts a function over some structure to affect only the
values within. An applicative (a monoidal functor) leverages a structure and
function together while lifting the function over structure. A monad (an
applicative functor) creates new structure whilst in the process of lifting a
function over existing structure."

I think [https://xkcd.com/483/](https://xkcd.com/483/) should apply to
programming languages too. ;-)

But seriously, can someone put this in English? Is it really more complicated
than "apply a function to every object in a list"?

~~~
TheAsprngHacker
But "apply a function to every object in a list" _isn 't_ the definition that
the author is getting at!

Most programmers probably know "map" as being an operation takes a function
and a list and returns a list of the results of the function applied to each
element of the input list. However, the "map" operation can be generalized to
generic types other than lists, where it has different behavior but shares
certain properties. In functional programming, the idea of a functor gives a
name to this pattern.

The idea is that if you have some generic type `T a` covariant in `a`, there
is a function `map : (a -> b) -> T a -> T b` that can "lift" every function `f
: a -> b` into a function `map f : T a -> T b`. The lifting operation respects
function composition and identity.

However, what if the function has multiple arguments, e.g. `f : a -> (b ->
c)`? Then, `map f : T a -> T (b -> c)` An applicative functor lets you convert
that `T (b -> c)` into a `T b -> T c`. So, with applicative functors, you can
individually map each parameter of a multi-parameter function.

A monad is a functor that can be "flattened." It has an operation `join : T (T
a) -> T a`, as well as an operation `return : a -> T a`. A monad can be seen
as generalizing the idea of flattening a list, or generalizing async/await.

~~~
anko
I really like your description. One thing i don't understand though, is why
generalising these operations is considered a good thing?

In my day to day I spend a bit of time writing functional code, and a lot of
time reviewing it, and when you generalise in this manner it hides a lot of
the details of the algorithmic complexity. Is this operation happening in a
future? or is on a list? You could argue that the type signature will let you
know, but quite often it's inferred.

Suddenly I find myself needing an IDE just to do code reviews. People make
arguments about naming variables and suddenly we're back to using hungarian
notation.

I also find it's easy to make mistakes - you do a flatmap instead of a map and
the Nones just vanish. Or you're composing so many generic functions that the
intent of the code just disappears.

I guess i just wanted to see if it's just me.

~~~
rovolo
> is why generalising these operations is considered a good thing?

> Is this operation happening in a future? or is on a list? You could argue
> that the type signature will let you know, but quite often it's inferred.

1) You are generalizing the input of a function to multiple types. Many of the
functions you'd write wouldn't care whether you're using a List or a Future,
they would accept either. It's just like declaring your Java method to take a
Collection instead of an ArrayList.

2) You are getting more specific in the output of the function. If you write
`filter`, it would be nice to get a `LinkedList` back if you pass in a
`LinkedList`. With more common type systems, the best you'll be able to do
writing the function once is a return type of `Collection` or `Iterable`.

Your method may perform horribly if a LinkedList is passed in instead of an
ArrayList. If your operation really depends on a specific behavior the Functor
doesn't specify, you should be using a different type than Functor.

~~~
sweeneyrod
> Many of the functions you'd write wouldn't care whether you're using a List
> or a Future, they would accept either.

I can't imagine a situation where that would be true, except if you're writing
a monad library. You could say that you might want to interchange a list of
futures for a future of lists, but that's two interdependent things being
swapped not one.

~~~
Silhouette
You've made at least two comments now mentioning this idea of interchange, but
there's more to all of this than just that. The Haskell standard libraries are
full of generic functions that can be applied to many practically useful types
as long as those types provide certain basic guarantees.

For example, there is a useful little function

    
    
        replicate :: Int -> a -> Seq a
    

that allows us to build a sequence of _n_ of some value. But what if we don't
have a raw value, but instead some Applicative wrapper around it, and instead
of just getting a sequence of individually wrapped values, we want that
Applicative structure around the sequence? That is, we want some function f
that will give us

    
    
        f 3 (Just 1) == Just (fromList [1, 1, 1])
    

but

    
    
        f 3 Nothing == Nothing
    

with a Maybe, and

    
    
        f 3 (Right 'x') == Right (fromList "xxx")
    

but

    
    
        f 3 (Left "error message") == Left "error message"
    

with an Either, and similarly for any other Applicative.

Well, such a function also exists in Data.Sequence:

    
    
        replicateA :: Applicative f => Int -> f a -> f (Seq a)
    

Because it works with any Applicative, it can just as well work out all of the
possible outcomes when we roll three dice:

    
    
        replicateA 3 [1, 2, 3, 4, 5, 6]
    

or repeat the action of any Monad three times:

    
    
        replicateA 3 $ putStrLn "Hello, world!"
    

or repeat whatever behaviour some Applicative you haven't even written yet
will have next week.

The rabbit hole gets as deep as you like. For example, we can easily compose a
variety of other standard functions to count how many ways there are to reach
each possible total in that dice example:

    
    
        map (head &&& length) $ group $ sort $ fmap sum $ replicateA 3 [1, 2, 3, 4, 5, 6]
        -- [(3,1),(4,3),(5,6),(6,10), ...]
    

Many of these functions have quite general types, for example sum working on
any Foldable and fmap working on any Functor. Because a list is an Applicative
and any Applicative is also a Functor, and since the individual results of the
replicateA will be Seqs and any Seq is also a Foldable, everything works fine,
and the list structure we started with is preserved all the way through.

------
ngcc_hk
Any js-er review?

~~~
yingw787
If you mean Javascript to Erlang, probably not for the foreseeable future. I
want to learn to ship product for a bit, and hopefully I can talk about that
:)

~~~
hopia
What kind of products are you building?

~~~
yingw787
It’s under wraps right now (mostly because I can’t look people who believed in
me in the eye if I fail) but it’s a tiny CRM for software engineers. Feel free
to subscribe to my mailing list on the blog for more information, or email me
at me@yingw787.com :)

------
Areading314
This page made me permanently write off Haskell:

[https://wiki.haskell.org/Prime_numbers](https://wiki.haskell.org/Prime_numbers)

It is so much work, so much effort to develop some very mediocre, very slow,
and incomprehensible implementations of an undergraduate level algorithm. I
ended up picking CL for the project I was working on (Project Euler) and never
looked back.

~~~
yingw787
You are of course entitled to your own opinion, but I would say that’s
unfairly harsh towards Haskell. GHC Haskell not only compiles down to a
binary, it can also transpile to C—-, both of which can run performantly.

I started off FP with lodash, then moved to Python and built in functions, and
I’ve tasted the benefits of applying functional programming in production, and
so that’s what makes learning Haskell interesting and worthwhile to me. I’m
looking forward to bringing what I’ve learned from Haskell towards the rest of
the code I write, even if it’s not in Haskell.

~~~
Areading314
I write Python for a living, and it is an astoundingly productive language.
What it gets right is the compromise between functional, object-oriented, and
imperative styles. I too have been fascinated by the "pure functional" type
languages, but they always fall short when it comes to "real world" projects.

~~~
yingw787
I think it’s definitely hard for practitioners to adjust to FP, especially
when under the pressure to ship. I think a great opportunity to look for ways
to apply FP would be in small utilities or libraries. I’m planning my next
project in Python but if I’m fortunate to ship that I’ll see if I can circle
back to Haskell and go from beginner to intermediate by building a small
library.

~~~
Areading314
If you're interested in FP, I highly recommend checking out Let Over Lambda.
It made me realize that there is actually lot in common between OO and FP.

[https://letoverlambda.com/](https://letoverlambda.com/)

