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

It did to me.

He slams go for not doing stuff the Haskell-way (e.g. pure code vs effectful code).

This is not how you approach new things



I've never written Haskell or another pure functional language more than a couple of lines, but the more I write code the more convinced I become that the ability to reason about mutability and side effects is a major force multiplier in writing robust software.


I tend to agree, but, even then, one doesn't necessarily have to take things quite as far as Haskell does.

In Rust, for example, mutable data is allowed, but, when the owner of mutable data shares a reference to it, it gets to decide whether the borrower is also allowed to mutate the data. This doesn't eliminate the more challenging things you can do with shared mutable variables, but it does mean that enabling them requires mutual consent.

Nim does an interesting thing, too. It has a two-color function mechanism where "procedures" are allowed to have side effects, and "functions" are not. But even functions are allowed to use mutable variables behind closed doors. That can arguably be an ergonomic win. Many people find that, within a sufficiently bounded context, an implementation that uses mutable variables might be more maintainable than a purely functional implementation.

The main reason Haskell goes even further, and bans mutable variables from the insides of functions as well, was never really about maintainability, per se. It was done that way because Haskell, as a lazy language, couldn't allow anything that might require statements to be evaluated in a particular order. That design decision turned out to lead to an impressive bounty of interesting and useful discoveries. But there also seems to be something of a tendency to swaddle the bathwater with the baby.


It's worth noting that Haskell allows mutable variables in IO actions - a type used to model computations that are not referentially transparent. It's just that using this facility is not super ergonomic.


I agree that being able to reason about mutability and other effects is useful. However, that doesn’t necessarily imply an all-or-nothing approach where either you’re in a pure function or you can do anything with anything. In a sibling comment, gwd mentioned const in C, which is one example of something in between. Rust’s ownership semantics and borrow checker are another.


> However, that doesn’t necessarily imply an all-or-nothing approach where either you’re in a pure function or you can do anything with anything.

To elaborate this point I'd say that the most important practical use of all that "monad mumbo-jumbo" in Haskell is that you can tag your functions with what they can and can't do and then the type system tracks this for you:

  -- pure function
  f1 :: Text -> Int

  -- can fail
  f2 :: Text -> Maybe Int

  -- can read from some MyEnv record
  f3 :: Text -> Reader MyEnv Int 

  -- can keep a set of bool as state around
  f4 :: Text -> State (Set Bool) Int
etc... and of course to go nuclear:

  -- can do anything
  f5 :: Text -> IO Int
The tracking part is that you can't call f5 from within f1, the type checker says no. It enforces separation between all these various effect boundaries.

Also we don't have to stop here. A natural next step is defining exact effects one is after. For example say I want my function to be able to get some entity from a DB:

  -- any type that is an instance of Entity can identify itself by uuid
  class Entity e where
    identify :: e -> UUID

  -- The effect we want, ie. get an entity via its uuid
  class (Monad m, Entity e) => GetEntity e m where
    getEntityById :: UUID -> m (Maybe e)
now we can say things like:

  data User = MkUser {uId :: UUID, uName :: Text}

  -- a User can identify itself
  instance Entity User where
    identify = uId

  -- as User is an Entity so we can get it via its uuid (if it exists)
  getUserById :: (GetEntity User m) => UUID -> m (Maybe User)
  getUserById = getEntityById
Also notice how getUserById does not say anything about IO or a DB. All it states is that whatever context it will run in that context must know how to get a user via its uuid. You can then plug in whatever actual context you want, say:

  newtype Prod a = MkProd {unProd :: ReaderT ProdDB IO a}
    deriving newtype (Functor, Applicative, Monad, MonadReader ProdDB, MonadIO)

  -- get a user from a DB for real
  instance GetEntity User Prod where
    getEntityById :: UUID -> Prod (Maybe User)
    getEntityById eid = do
      ProdDB {..} <- ask
      -- query the DB, etc...
or

  newtype Mock a = MkMock {unMock :: State (Map UUID User) a}
    deriving newtype (Functor, Applicative, Monad, MonadState (Map UUID User))

  -- get a user from a mock DB
  instance GetEntity User Mock where
    getEntityById :: UUID -> Mock (Maybe User)
    getEntityById eid = gets (Map.lookup eid)
All in all the programmer have fine-grained control over what various parts of their code can or can't do.


The trick seems to be how to implement this kind of idea in a composable way, so we can still have a clear, modular design and write generic, reusable code even in the presence of several different types of effect. IMHO, the research into type and effect systems in recent years has been promising, but also shows that this is not an easy problem to solve. I’m hoping that in a few more years we might see a new generation of programming languages that incorporate this kind of “effect safety” as easily and naturally as many developers use const and qualifiers and resource management idioms today.


I haven't yet looked much into effect systems so can't really talk about them but I think even with something like MTL you can achieve a decent level composability/modularity where you build up a library of capabilities that you can then use to pick and choose effects from, eg.:

  type FooContext m = (GetTime m, GenUUID m, Log m, Auth m, ReadDB m, WriteS3 m, BarApi m, etc...)

  foo :: (FooContext m) => UUID -> m Foo
The main downside is ergonomics, ie. the amount of boilerplate needed, but personally I don't think that's a high price to pay.


Unison's ability system (an implementation of algebraic effects) is looking promising as an ergonomic method of tracking effects.


> He slams go for not doing stuff the Haskell-way (e.g. pure code vs effectful code).

Heck, I come from a C background, and that's a complaint I have about Go. Sometimes you want to have a function accept a pointer to a large structure to avoid copying, but have the compiler prevent you from making any changes. In C you'd write "const"; in Go there's no way to do that.


Sure, const is great to have.

But Haskells view on this topic is far far more opinionated than just supporting 'const'.


That is a single dubious point amongst some serious and in my opinion legitimate issues with the language.


Complaining about strict evaluation, the std lib not doing what he wants it to, the type system, the lack of heap profiler/threadscape, and .... demanding Golang handle zero values in his preferred way is basically him whining it ain't Haskell. I could blubber about it not having power conjunctions or self intervals or whatever if I wanted Golang to be J, which would be a similarly moronic roster of complaints. In reality golang is a basket of compromises like any other programming language. There's vastly more good stuff built in it than Haskell, despite Haskell being older and more theoretically amazing, so they must be doing something right.

His only criticism that resonated with me was go get defaulting to head, which befuddled me when I first saw it, but that's a compromise too; one oriented to the large codebases that golang is designed for. It's actually a pretty good compromise compared to the propeller head Haskell dork "Things break backwards compatibility in Haskell, and the users just update their code because they know the library author did it for a reason." -aka this appears to be a statement from an academic wanker who obviously never had to ship on a timeline in his entire fucking life.


The type system complaints are somewhat in the area of "it should be more like Haskell", but the debugger, heap profiler and thread debugger ones are certainly not.

Golang has extremely basic tools for anything beyond formatting/compiling compared to most modern languages. This is just a limitation of the current ecosystem, not something you can say has tradeoffs (except of course for the Go team's time/prioritization of features).

Delve is getting better, but it has a LOT of ground to cover before it is half as useful as gdb or a java/C# debugger. Same goes for the profiling tools, especially on the CPU side.


I've never really done Haskell, but I have Erlang, Java, C#, JS, TS and Erlang under my belt.

I agree with everything the author said after having spend some time with Go.




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

Search: