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
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.
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. 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.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.
But Haskells view on this topic is far far more opinionated than just supporting 'const'.
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.
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 agree with everything the author said after having spend some time with Go.