Comment by AndyKelley

Comment by AndyKelley 16 hours ago

14 replies

It's not a monad because it doesn't return a description of how to carry out I/O that is performed by a separate system; it does the I/O inside the function before returning. That's a regular old interface, not a monad.

endgame 16 hours ago

So it's the reader monad, then? ;-)

  • [removed] 16 hours ago
    [deleted]
  • tylerhou 13 hours ago

    Yes.

    • AndyKelley 12 hours ago

      Can you explain for those of us less familiar with Haskell (and monads in general)?

      • themk 8 hours ago

        A Monad is a _super_ generic interface that can be implemented for a whole bunch of structures/types. When people talk about "monads", they are usually referring to a specific instance. In this case, the Reader monad is a specific instance that is roughly equivalent to functions that take an argument of a particular type and return a result of any type. That is, any function that looks like this (r -> a) where `r` is fixed to some type, and `a` can be anything.

        Functions of that form can actually implement the Monad interface, and can make use of Haskells syntax support for them.

        One common use-case for the reader monad pattern is to ship around an interface type (say, a struct with a bunch of functions or other data in it). So, what people are saying here is that passing around a the `Io` type as a function argument is just the "reader monad" pattern in Haskell.

        And, if you hand-wave a bit, this is actually how Haskell's IO is implemented. There is a RealWorld type, which with a bit of hand waving, seems to pretty much be your `Io` type.

        Now, the details of passing around that RealWorld type is hidden in Haskell behind the IO type, So, you don't see the `RealWorld` argument passed into the `putStrLn` function. Instead, the `putStrLn` function is of type `String -> IO ()`. But you can, think of `IO ()` as being equivalent to `RealWorld -> ()`, and if you substitute that in you see the `String -> RealWorld -> ()` type that is similar to how it appears you are doing it in Zig.

        So, you can see that Zig's Io type is not the reader monad, but the pattern of having functions take it as an argument is.

        Hopefully that helps.

        ---

        Due to Haskell's laziness, IO isn't actually the reader monad, but actually more closely related to the state monad, but in a strict language that wouldn't be required.

      • itishappy 9 hours ago

        A reader is just an interface that allows you to build up a computation that will eventually take an environment as a parameter and return a value.

        Here's the magic:

            newtype Reader env a = Reader { runReader :: env -> a }
            
            ask = Reader $ \x -> x
            
            instance Functor (Reader env) where
              fmap f (Reader g) = Reader $ \x -> f (g x)
            
            instance Applicative (Reader env) where
              pure x = Reader (\_ -> x)
              ff <*> fx = Reader $ \x -> (runReader ff x) (runReader fx x)
            
            instance Monad (Reader env) where
              (Reader f) >>= g = Reader $ \x -> runReader (g (f x)) x
        
        That Monad instance might be the scariest bit if you're unfamiliar with Haskell. The (>>=) function takes a Monad (here a Reader) and a continuation to call on it's contents. It then threads the environment through both.

        Might be used like this:

            calc :: Reader String Int
            calc = do
              input <- ask
              pure $ length input
            
            test :: Int
            test = runReader calc "Test"
            -- returns: 4
        
        Not sure how this compares to Zig!

        https://stackoverflow.com/questions/14178889/what-is-the-pur...

        Edit: Added Applicative instance so code runs on modern Haskell. Please critique! Also added example.

        • itishappy 6 hours ago

          Here's a minimal python translation of the important bits:

              class Reader:
                  def __init__(self, func):
                      self.run = func
                  def pure(x):
                      return Reader(lambda _: x)
                  def bind(self, f):
                      return Reader(lambda env: f(self.run(env)).run(env))
          
              ask = Reader(lambda env: env)
          
              def calc():
                  return ask.bind(lambda input_str:
                      Reader.pure(len(input_str)))
          
              test = calc().run("test")
              print(test)
          
          Admittedly this is a bit unwieldy in Python. Haskell's `do` notation desugars to repeated binds (and therefore requires something to be a Monad), and does a lot of handiwork.

              -- this:
              calc :: Reader String Int
              calc = do
                input <- ask
                pure $ length input
          
              -- translates to:
              calc' :: Reader String Int
              calc' = ask >>= (\input -> pure $ length input)
      • throwaway17_17 10 hours ago

        I see I’ve been beaten to the punch, but I’ll post my try anyway.

        Your comment about IO handled by an external system In response to a comment about the more general concept of a monad is what they are, somewhat abruptly referring to in the above two comments.

        The IO monad in Haskell is somewhat ‘magical’ in that it encapsulates a particular monad instance that encodes computational actions which Haskell defers to an external system to execute. Haskell chose to encode this using a monadic structure.

        To be a bit more particular:

        The Reader monad is the Haskell Monad instance for what can generically be called an ‘environment’ monad. It is the pattern of using monadic structure to encapsulate the idea of a calling context and then taking functions that do not take a Context variable and using the encapsulating Monad to provide the context for usage within that function that needs it.

        Based on your streams in the new system I don’t see a monad, mostly because the Reader instance would basically pipe the IO parameter through functions for you and Zig requires explicit passage of the IO (unless you set a global variable as IO but that’s not a monad, that’s just global state) to each function that uses it.

        From my perspective Zig’s IO looks to be more akin to a passed effect token outside the type system ‘proper’ that remains compile time checked by special case.

      • dan-robertson 10 hours ago

        Reader monad is a fancy way of saying ‘have the ability to read some constant value throughout the computation’. So here they mean the io value that is passed between functions.

      • _jackdk_ 10 hours ago

        Let's see if I can do it without going too far off the deep end. I think your description of the _IO type_ as "a description of how to carry out I/O that is performed by a separate system" is quite fair. But that is a property of the IO type, not of monads. A monad in programming is often thought of as a type constructor M (that takes and returns a type), along with some functions that satisfy certain conditions (called the "monad laws").

        The `IO` type is a type constructor of one argument (a type), and returns a type: we say that it has kind `Type -> Type`, using the word "kind" to mean something like "the 'type' of a type". (I would also think of the Zig function `std.ArrayList` as a type constructor, in case that's correct and useful to you.) `IO String` is the type of a potentially side-effecting computation that produces a `String`, which can be fed to other `IO`-using functions. `readLine` is an example of a value that has this type.

        The Haskell function arrow `(->)` is also a type constructor, but of two arguments. If you provide `(->)` with two types `a` and `b`, you get the type of functions from `a` to `b`:

        `(->)` has kind `Type -> Type -> Type`.

        `(->) Char` has kind `Type -> Type`.

        `(->) Char Bool` has kind `Type`. It is more often written `Char -> Bool`. `isUpper` is an example of a value that has this type.

        The partially-applied type constructor `(->) r`, read as the "type constructor for functions that accept `r`", is of the same kind as `IO`: `Type -> Type`. It also turns out that you can implement the functions required by the monad interface for `(->) r` in a way that satisfies the necessary conditions to call it a monad, and this is often called the "reader monad". Using the monad interface with this type constructor results in code that "automatically" passes a value to the first argument of functions being used in the computation. This sometimes gets used to pass around a configuration structure between a number of functions, without having to write that plumbing by hand. Using the monad interface with the `IO` type results in the construction of larger side-effecting computations. There are many other monads, and the payoff of naming the "monad" concept in a language like Haskell is that you can write functions which work over values in _any_ monad, regardless of which specific one it is.

        I tried to keep this brief-ish but I wasn't sure which parts needed explanation, and I didn't want to pull on all the threads and make a giant essay that nobody will read. I hope it's useful to you. If you want clarification, please let me know.