Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

I dislike Haskell. But this article goes out of its way to make the worst possible case for Haskell imaginable.

> Many programmers encounter statically typed languages like Java or C++ and find that the compiler feels like an annoyance. By contrast, Haskell’s static type system, in conjunction with compile-type time checking, acts as an invaluable pair-programming buddy that gives instantaneous feedback during development.

Many programmers find that Java or C++'s static type system, in conjunction with with compile-type time checking feels like an annoyance. Unlike... the very same statement about Haskell? That's... that's quite a weak claim, to say the least.

> a signature like Int -> Int -> Bool indicates that a function takes two integers and returns a boolean value... this allows a programmer reading Haskell code to look only at type signatures when getting a sense of what a certain piece of code does. For example, one would not use the type signature above when looking for a function that manipulates strings, decodes JSON, or queries a database.

So... Type signature `Int -> Int -> Bool` can be used for a function that does any of the following things: manipulates strings, decodes JSON, or queries a database? How does that make it easier to deduce what a function does by "looking only at type signature"?

> Another feature of a pure functional programming paradigm is higher-order functions, which are functions that take functions as parameters.

As in: available in almost any language these days, and not exclusive to a "pure functional programming paradigm".

> One of the common development workflows we employ is relies on a tool called ghcid, a simple command line tool that relies on the Haskell repl to automatically watch code for changes and incrementally recompile. This allows us to see any compiler errors in our code immediately after saving changes to a file. It’s not uncommon for us to open only a terminal with a text editor and ghcid while developing applications in Haskell.

As in: Modern IDEs don't require you to run external tools to monitor your code for changes and highlight errors.

> a common refactoring workflow is to make a desired change in one location and then fix one compiler error at a time until the program compiles again.

As in: Modern IDEs let you do large-scale refactoring in one go, at a press of a button.

> The type system can protect us from making mistakes when changing the rules of our domain.

It can't. The example provided can't stop you from doing `case status of Paid -> delete invoiceNumber`. You have to invest significantly in a type-based DSL to prevent that from happening. But then, who will test your DSL?

> Haskell enables domain-specific languages, which foster expressiveness and reduce boilerplate

DSLs where all the rage 5-10 years ago. In reality, they are overhyped and are used very sparingly, for obvious reasons: DSLs are languages. They have to be designed, developed, maintained. Errors in your DSL will most likely harder to find and debug than in your regular program.



To clarify the sibling, a signature of `Int -> Int -> Bool` can't do any IO (so no connecting to databases, reading/writing files, network requests), so it does tell you a lot about a function.

It can manipulate strings, decode JSON, but both of these are either (immutable) values from enclosing scope, or created within the function. But since they can't be output anywhere (because no IO) then they don't matter.

EDIT:

I'll just add a few more counterpoints.

Agreed that DSLs are difficult, and often not worth the trouble. But if you do want to create a DSL, then haskell is a good fit because of monads and monad transformer stacks - i.e you can make the statement mean whatever you want it to mean, and keep the effects in check with types.

The synergy between higher order functions with typed IO is great, better than in other languages.

Agreed that IDE refactoring is convenient, and also refactoring in other languages with static type checking the process is similar to what they describe in the article, so no immediate "pro" there.


> To clarify the sibling, a signature of `Int -> Int -> Bool` can't do any IO (so no connecting to databases, reading/writing files, network requests), so it does tell you a lot about a function.

It doesn't help if it's `Int -> Int -> IO Bool`, for example. Well, it does do IO, but other than that, who knows. Perhaps it reformats the disc while CPU is idle :)

My main point though is that the article does a very poor job of showing why Haskell is good at, well, anything, compared to, well, anything.


It's the fact that it is `Int -> Int -> Bool` that ensures you that the function doesn't do any random thing (like doing something on the database). Combining non-IO capable functions is quite helpful in reasoning about programs. I.e if the function has `IO` in the result type then exactly - "who knows". But with the fact that it doesn't, you do know that it doesn't do anything other than return the bool.


I mean. Really. That's what people hang up on?

The point for that particular gripe was this: "this allows a programmer reading Haskell code to look only at type signatures when getting a sense of what a certain piece of code does."

Yes, IO tells that ... the function does some side effects. And that's it. If your type signature is `Int -> Int -> IO Bool`, it's just as useless as `Int -> Int -> Bool`, and requires you to read the function to understand what it actually does.

Is it helpful to see at a glance which functions produce side effects and which don't? Yes, it is. Does it automagically "allow a programmer reading Haskell code to look only at type signatures when getting a sense of what a certain piece of code does"? No, no, it doesn't. `... -> IO Bool` may be launching nukes for all the information you glean from its type signature.


You don't get any information from `IO _` functions, but that's the case with every mainstream language, where you have an implicit `IO` on every single function.

The benefit is not in `IO` -- its that its absence on a function tells you that it is heavily restricted in what it can do.

If you want to go the other way, you can concretely model the kinds of effects you need parts of your programs to do, and use that model instead of `IO`. Even if you end up implementing it using `IO`, you know it can't use more than what your model exposes.

Both of these possibilities, while not truly unique (see Agda, etc.), are absolutely rare-to-nonexistent in mainstream languages. Effect modeling is a real shift in perspective.


> You don't get any information from `IO _` functions, but that's the case with every mainstream language, where you have an implicit `IO` on every single function.

Indeed. And that's why I picked up on that particular point in the article that I'm criticising. The article chose to use those examples and those words and they don't show anything beyond what other mainstream languages have.

> If you want to go the other way, you can concretely model the kinds of effects you need parts of your programs to do, and use that model instead

Yes, you can. No, the article doesn't show that in any way. Just to remind you how I started my comment:

--- start quote ---

But this article goes out of its way to make the worst possible case for Haskell imaginable.

--- end quote ---

Does the article show "other models"? No. Does it even try and show how to reason about a function by looking at its type signature alone? Also, no. Would this article be laughed into oblivion had it been about any other language but Haskell? Yes, most likely.

> Effect modeling is a real shift in perspective.

Ah yes. Does the article talk about this? Does it show a single example of this? No.


Having a distinction of if something does IO or not is quite important, and the fact that haskell has the ability to encode it in types is what other languages don't have.

Secondly, IO is just one monad. You can build a more granular one where you can separate filesystem, network etc, and encode this into types (so you know at a glance). You can't do this in most other languages.

What are the types of things that are worth being hung up on for a language for you?


Answered in a sibling comment: https://news.ycombinator.com/item?id=25729423


I guess you haven’t spend too much time with Haskell. Haskell types especially generic ones are way more expressive and limit your search quite a bit. The function Int -> Int -> Bool can not reach for a database or anything other than the two Ints it is given.


Why couldn't it be a function that queries a database, like answering "on invoice x, does line y exist"?

Genuinely curious, not that familiar with Haskell, just thought you could use something like parameter binding or similar to construct functions like that.


Haskell functions are pure, which means they can only access/use what is in their parameters, so unless you pass in some extra context (typically using a monad or an effect), you do not have access to the "outside world".


You can bind parameters though in Haskell, no?

I'm used to Boost.Bind and similar, so was thinking a scenario where you bind the database connection parameter and pass the resulting function to something else.

As the sibling pointed out though, I now get that the result would be "tainted" so to speak.


A database connection would operate either in IO (the generic "i am now talking to an unreliable outside world") monad, or some more specific monad.

In this case you can think of a monad a bit like a computational context. If one is not present, you simply cannot[^1] instruct a Haskell program to perform those operations in a type safe way even if you give it a valid database connection identifier.

[1] Well, you can, but if you do you're explicitly taking away all the safeguards that Haskell introduces, and it would never pass code review.


if it has side-effects, it has to be something like

  Int -> Int -> IO Bool
otherwise the type checker won't let you perform any side-effecting operations

(btw Haskell's `IO Bool` would be spelled `IO<Bool>` in C++/Java syntax)


And there's no way to get rid of the IO then, presumably (I mean otherwise I could have just used that as a wrapper).

edit: So at work, just about every value depends directly or indirectly on stuff that comes from files or from the database. So would they all have to be wrapped by IO?


In Haskell, you often build pure transformations, and then lift them into an effectful context. If I have a function `String -> [String]`, say to parse a line of CSV into its elements, I can lift that to `IO String -> IO [String]` using the IO monad's `fmap`. And then I can compose it with something hypothetical like `readLine :: File -> IO String`, which actually reads the line.

The core logic of a program often doesn't need to care deeply about state or system resources. Pure functional programming is about writing as much as you can in this "functional core", and then lifting the assembled pieces of pipeline into the "imperative shell" (such as the IO monad).


Ok, so you make pure functions and turn them dirty, so to speak. Makes sense.

In our case, almost all core code depends on various parameters, which come from the database.

For example, GB recently left the EU so everything involving GB is now processed under different rules, except old stuff which has to be processed under the old rules. Thus being part of EU or not is a date-dependent database query (it already was, not the first time a country's EU status has changed).

So if I get your explanation correctly, I'd code the core logic as if these parameters were pure, side-effect free, which would make the core logic side-effect free. In the case above, I'd pass a function which maps a (pure) date and string into a (pure) bool, to test for EU membership.

I'd then turn that whole thing dirty via the IO thingy, passing "IO parameters" and receiving "IO results", so I can pass it my EU test function which does a database query.

edit: And I presume my "dirty" database-connecting function can also mutate things, so it can do caching. Don't want to hit that database too often.


> In the case above, I'd pass a function which maps a (pure) date and string into a (pure) bool, to test for EU membership.

i doubt you could make (or really, even want to make) `checkEUMembership` pure, I'm guessing it'd involve a DB lookup of some kind.

in general, you can't always "pull out all the IO" into an only-pure-logic "core"; like if you want to look up one thing and then look up another thing based on the result of the first lookup. and that's okay!

i'm not going to write a whole monad tutorial, but using an `IO Foo` is kind of like using a `Promise<Foo>`¹; you do stuff like this (in JS syntax):

  getX(...).then((x) =>
    getYForX(x).then((y) =>
      foo(x, y)
      // note - nested lambdas/closures, `x` is closed-over
    )
  )
"do-notation" lets you avoid callback hell, similarly to async/await.

---

¹ Unfortunately, JS's Promise#then mixes two things:

• "dirtying" a pure function:

  getNumberFromDB().then((x) => x*2)
which in Haskell would use

  fmap :: (a -> b) -> IO a -> IO
• piping the result into another side-effecting function:

  getNumberFromDB().then((x) => 
    getNameForNumberFromDB(x)
  )
which in Haskell would use the "bind" operator:

  (>>=) :: IO a -> (a -> IO b) -> IO b


> i doubt you could make (or really, even want to make) `checkEUMembership` pure, I'm guessing it'd involve a DB lookup of some kind

Well that was kinda the root of my question. The core logic doesn't really care as such, as long as it could determine EU membership somehow, but actual code would have to use a DB lookup[1].

That of course spirals back to what would that really buy you. You'd write code pretending it's pure while it really isn't. I can see part of the appeal, but I can do that in my current language.

Of course I don't get an error if I do something silly in the middle of some otherwise "pure" module, so there's that.

Anyway, illuminating. I enjoy thinking about these things and challenging my self-taught ways. Thank you all for your contributions, much appreciated!

[1]: An aside but, due to an error on the government side, GB is part of EU today as far as one of their validation checks is concerned. So today only we have to pretend along, for just that one field. This stuff is fun!


> You'd write code pretending it's pure while it really isn't.

in a way, it's the opposite! the point is you can't pretend, you have to make impurity painfully explicit:

  getTradeTax :: CountryId -> CountryId -> IO Float
  --                                       ^ sirens blaring, side-effect alert
  getTradeTax ca cb = do
    aInEu <- lookupEUMemberDB ca
    bInEu <- lookupEUMemberDB cb
    if (aInEu && bInEu)
      then (pure 15.00)           -- made up value
      else getNonEUTradeTax ca cb -- another impure operation
there's the "IO" in the signature, and all the do-notation `<-`, ie syntactic sugar for `>>=`, piping the result into a callback. to use the Promise analogy again, `x <- foo` is kinda like `x = await foo` (but more general, bc Monads are cool)

> I can see part of the appeal, but I can do that in my current language.

true, and i've seen IO-monad-alikes for Python and JS, but most of the benefits come when every library you use has to be explicit about impurity and there's a typechecker enforcing it.


As a concrete example, there's a nice function in Haskell's standard library called `interact :: (String -> String) -> IO ()`: https://hackage.haskell.org/package/base-4.14.1.0/docs/Prelu...

Its argument is a function of type `String -> String` and it returns an `IO ()`, i.e. an i/o action with a trivial result. That action will call the given function on contents of stdin, and writes its result to stdout. Or, equivalently, we can think of `interact f` as transforming a pure string-processing function `f` into a stdio CLI.

Note that laziness (specifically "lazy IO") causes stdin to be read 'on demand', giving us a streaming computation without any extra work. Here's an example implementation of 'wc':

    module Main where
    import System.IO

    main :: IO ()
    main = interact count

    count :: String -> String
    count input = show (length (unwords input))
Bonus: if we want to show off, we could implement 'count' using function composition like this:

    count = show . length . unwords


But is _just reading_ from a database a "side-effect"? It is non-deterministic, but it has no side-effects. So IO jumbles together the two notions.


pedantically, talking to the database will involve sending a request or some kind of IPC, all of which are usually considered side effects.

[handwavy analogy alert]

people often call it "side-effects" as a shorthand. in reality an expression being of type `IO Foo` mostly just tells the compiler that order of execution matters (which isn't the case with pure functions):

  do print "a"
     print "b"
 
  -- obviously not the same as
  
  do print "b"
     print "a"
and also that it can't eliminate common expressions:

  do a <- readBytes file 100
     b <- readBytes file 100
     doStuff a b

  -- obviously not the same as

  do x <- readBytes file 100
     doStuff x x
it's a way of enforcing ordering in a lazy language where evaluation order isn't really defined.


I guess I read the article and commented on what the article was saying and claiming. Hence the introductory sentences at the top of my post.


Perhaps when you wrote

> Type signature `Int -> Int -> Bool` can be used for a function that does any of the following things

[My emphasis] you meant "can't". That could be one explanation for the confusion that seems to have arisen here.


> So... Type signature `Int -> Int -> Bool` can be used for a function that does any of the following things: manipulates strings, decodes JSON, or queries a database

It can definitely NOT query a database(as that would be an effect which would be visible in the type).


> So... Type signature `Int -> Int -> Bool` can be used for a function that does any of the following things: manipulates strings, decodes JSON, or queries a database? How does that make it easier to deduce what a function does by "looking only at type signature"?

From this quote, it's easy to see you've not spent any time actually using Haskell (others have explained what is actually going on), so why do you dislike it? I cannot fathom having an opinion either way on a language I don't know.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: