Mutability / side effects

hello! please forgive me if there is an obvious reference i haven’t yet found, but though my experience learning haskell has been very productive (i implemented do notation and some common monads in julia), i still have a few nagging questions about mutability and side-effects, specifically IO, and i would appreciate any help!

with mutability, i must have several concepts confused, but in c, for example, i have access to a mutation hierarchy, with some helpful syntax - primitive types are mutable → there are mutable arrays of mutable types → more elaborate mutable data structures can be constructed eg hash maps.

i have been told that in haskell, IORef provides mutability, and i have done some exercises with IORef and Array, and of course the mutability is hidden, which is good. how do i understand the ‘source’ of the mutability in haskell? is everything written in haskell? how do i know if something is mutable? how do i ‘extend’ mutability?

regarding IO, i think my question is similar - i understand that eg do notation is sugar for constructing a nested function with repeated monad/function applications - but what does the haskell runtime do when it runs the program? what is haskell giving my function that allows it access to 1) file pointers 2) stdin/out 3) sockets? how much of THIS is written in haskell?

with python i have an understanding that all the ‘interesting’ code is written in c, and that model makes sense to me. haskell, being a much more sophisticated enterprise, doesn’t seem to have this quality, but by virtue if it being functional, i am naturally left to wonder how it actually interacts with the outside world.

thank you!

I’ll answer a few of your questions, then provide some links.


Answers

  • How do i understand the ‘source’ of the mutability in Haskell?

    Mutability in Haskell uses the same mechanism as C, usually because:

    • the run-time system for Haskell, and

    • the primitive entities Haskell is based on (e.g. the Integer type and its arithmetic operators primPlusInteger, primMinusInteger, etc)

    are written in an imperative language like C.

  • Is everything written in Haskell?

    No. That would be impractical - that’s why Haskell has an FFI.

  • How do i know if something is mutable?

    Sometimes by looking at the name of the type e.g. IORef, STRef, MVar, STMVar. But there’s no way in general to know if something in Haskell is mutable.

  • What does the Haskell runtime do when it runs the program?

    The Haskell run-time system is “dual-purpose”:

    • it can call regular Haskell functions like (\ x y -> x && not y);

    • and run IO actions like putStrLn "Hello, World!" .

    You can see this in the type signature for IO's bind operator:

    (>>=) :: IO a         -- run this action to obtain a result,
          -> (a -> IO b)  -- then call this function, using the result as the argument,
          -> IO b         -- to build another action.
    

    What the run-time system selects then depends on what it’s presented with moment-to-moment as the Haskell program runs.

  • […] I am naturally left to wonder how [Haskell] actually interacts with the outside world.

    Without going into all the possible ways IO can be implemented: very carefully! There are good reasons why the Haskell 2010 report (page 95 of 329) deems IO to be abstract, so until that requirement changes (e.g. in a future version of Haskell) I recommend either:

    • ignoring how IO is implemented altogether - so far, you’ve managed to get by without wondering how (->) is implemented;

    • or looking at several implementations of IO e.g. in How to Declare an Imperative to better understand the advantages and difficulties of each implementation method.


Links

From the wiki:

  1. Introducing I/O

  2. Mutable variable - HaskellWiki

  3. IO inside - HaskellWiki

1 Like

I’m sure many people will jump in with detailed and technical answers, but I’ll just provide a couple of high level remarks. Re. mutability, Haskell does indeed provide things like IORef which can be mutated by performing an IO action, but overwhelmingly prefers solutions to problems that don’t require mutable data structures at all. Often, coming from imperative languages, this requires pretty heavy rethinking of the approach to a problem - see for example https://www.amazon.com/Purely-Functional-Data-Structures-Okasaki/dp/0521663504.

This isn’t an answer because I’m sure you’ll be getting a ton of them, but you might find The C language is purely functional interesting since it’s fairly related to the topic. (The post is half satire/joking, but the point is that sometimes the answer to these kinds of questions can just be a matter of perspective.) In particular:

“C programmers” really program not in C, but in the purely functional language cpp (the “C Preprocessor”). As with any purely functional language, cpp consists only of (nestable) expressions , not statements. Now here’s the clever part: when evaluated (not executed), a cpp expression yields a (pure) value of type C , which is an ADT (abstract data type) that represents imperative programs. That value of type C is then executed (not evaluated) by the cpp language’s RTS (run-time system). As a clever optimization, cpp’s RTS actually includes a code generator, considerably improving performance over more naïve implementations of the C abstract data type.

  • The C ADT is implemented simply as String (or char *, for you type theorists, using a notation from Kleene), and the representation is exposed rather than hidden, so that cpp programs operate directly on strings. The wisdom of data abstraction caught on later.

Fortunately for purists, the relatively obscure programming language Haskell restored the original untarnished beauty of K&R’s vision by fixing these defects. The type system was modernized, scoping rules improved, and the C type (renamed to “IO“, to avoid expensive legal battles) made abstract.

Again, there are a lot of jokes in there, but the point is salient even if it’s just an exercise in looking at familiar things from a different perspective.

2 Likes

I’m a bit confused. Doesn’t abstraction mean that the definition is hidden and inaccesible? Why would a definition of IO via the FFI like that count as an abstraction?

As per the usual definition of abstraction in Haskell, the IO type is abstract in the sense that its definition is only exported from internal modules (although those are annoyingly not obviously marked as such).

If you want proper abstractions, I would suggest using backpack and writing everything as modules that are parameterized over an IO signature.

2 Likes

If you accept that Python delegates mutation to a runtime system written in C, then the same can be said of a Haskell interpreter/compiler.

Although, I don’t find that a useful thing to know. (Plus the slippery slope of “but where does C get its mutation from, and why can’t everyone obtain it from the same source directly?”)

IMO the useful thing to know is: Your computer can do very arbitrary things, but it’s the job of language design to selectively, in a disciplined way, expose that to you. Reason being that a little bit of restriction can go a long way in making code comprehensible.

this is tremendously helpful especially @atravers thank you!