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
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)
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.
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.
Reader monads have been used to implement dependency injection in Haskell and Scala libraries. A monad in general is the ability to compose two functions that have pure arguments and return values that encode some effect... in this case the effect is simply to pass along some read only environment.
Based on my understanding of above, passing an environment as a parameter is not the Reader monad, in fact passing the parameter explicitly through chains of function calls is what the Reader monad intends to avoid in typed, pure functional programming.
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.
Well I don't think that fits at all. In Zig, an Io instance is an interface, passed as a parameter. You can draw some connections between what Zig is doing and what Haskell is doing but it's not a monad. It's plain old interfaces and parameters, just like Allocator.
Passing an interface as a parameter is a monad. (Io -> _) is an instance of Monad in Haskell.
Haskell just has syntax to make using (any) monad much nicer. In this case, it let's you elide the `Io` parameter in the syntax if you are just going to be passing the same Io to a bunch of other functions. But it still is there.
Couldn't have said it better myself. But IIUC Andrew stated that its not a monad because it does not build up a computation and then run. Rather, its as if every function runs a `runIO#` or `runReader` every time the io parameter is used.
Is it necessary that a monad "builds up a computation and then runs"? In fact it's very hard for a monad to do that because the type of bind is
(>>=) :: m a -> (a -> m b) -> m b
so you can really only make progress if you first build a bit (`m a`), then run it (to get `a`) then build the next bit (applying `a` to `a -> m b`), then run that. So "building" and "running" must necessarily be interleaved. It's an odd myth that "Haskell's IO purely builds an impure computation to run".
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.
This is pretty concise, but is still really technical. That aside, I think the actual bone of contention is that Zig’s IO is not a Reader-esque structure. The talks and articles I’ve read indicate that function needing the IO ‘context’ must be passed said context as an argument. Excepting using a global variable to make it available everywhere, but as I said in a sibling comment, that’s just global state not a monad.
In a manner of speaking, Zig created the IO monad without the monad (which is basically just an effect token disconnected from the type system). Zig’s new mechanism take a large chunk of ‘side-effects’ and encapsulates them in a distinct and unique interface. This allows for a similar segregation of ‘pure’ and ‘side-effecting’ computations that logically unlined Haskell’s usage of IO. Zig however lacks the language/type system level support for syntactically and semantically using IO as an inescapable Monad instance. So, while the side effects are segregated via the IO parameter ‘token’ requirement they are still computed as with all Zig code. Finally, because Zig’s IO is not a special case of Monad there is no restriction on taking IO requiring results of a function and using them as ‘pure’ values.
The sun’s radiation hitting earth is 44,000 terawatts. I think we’re fine with an “extra” terawatt. (It’s not even extra, because it would be derived from the sun’s existing energy.)
At sea-level there can be 1.225 kg/m^3 of particles. There's a lot of matter to absorb heat.
In the exosphere we have 1e-13 kg/m^3 of particles.
My point is that the exosphere while huge has an incredibly tiny thermal battery. I'm not convinced that, were we able to dump heat into it, that it really would be insignificant heating over time.
And there's little way for the articles here to cool down. There's no matter to transfer their energy to.
I guess the thing is, it doesn't matter. It seems like the exosphere is actually already >500 degrees: that after you leave the 80km menopause temperatures soar quickly, in what scant air is left. I was still using a model of thermal transfer. But the only cooling possible is passive radiative cooling, is to glow your energy away. Some of this will find other exospheric particles to hit & excite more, but they're already incredibly energetic up there, and there's just not many particles at all, so perhaps a lot of that radiation might escape the exosphere without collision. Again my mistake: thermal transfer is simply not that relevant (aside some shielding against these particles in vulnerable spots), it's all passive radiation being used to cool.
It would still be interesting to me to have some guestimates for what the current energy balance of the exosphere is. What is heating it, how much, and where/how-much is it able to dissipate its energy?
> I would like to learn category theory properly one day, at least to that kind of "advance undergraduate" level she mentions.
As someone who tried to learn category theory, and then did a mathematics degree, I think anyone who wants to properly learn category theory would benefit greatly from learning the surrounding mathematics first. The nontrivial examples in category theory come from group theory, ring theory, linear algebra, algebraic topology, etc.
For example, Set/Group/Ring have initial and final objects, but Field does not. Why? Really understanding requires at least some knowledge of ring/field theory.
What is an example of a nontrivial functor? The fundamental group is one. But appreciating the fundamental group requires ~3 semesters of math (analysis, topology, group theory, algebraic topology).
Why are opposite categories useful? They can greatly simplify arguments. For example, in linear algebra, it is easier to show that the row rank and column rank of a matrix are equal by showing that the dual/transpose operator is a functor from the opposite category.
Agreed. In addition to yours, notions like limits/colimits, equalisers/coequalisers, kernels/cokernels, epi/monic will be very hard to grasp a motivation for without a breadth of mathematical experience in other areas.
Like learning a language by strictly the grammar and having 0 vocabulary.
I should have mentioned in my post that I have an applied math masters and a solid amount of analysis and linear algebra with some group theory, set theory, and a smattering of topology (although no algebraic topology). So, I'm not coming to this with nothing, although I don't have the very deep well of abstract algebra training that a pure mathematician coming to category theory would have.
Although, it feels like category theory _ought_ to be approachable without all those years of advanced training in those other areas of math. Set theory is, up to a point. But maybe that isn't true and you're restricted to trivial examples unless you know groups and rings and fields etc.?
You could take a look at Topology: A Categorical Approach by Bradley, Bryson and Terilla.
It's a crisp, slim book, presenting topology categorically (so the title is appropriate). It both deepens the undergraduate-level understanding of topology and serves as an extended example of how category theory is actually used to clarify the conceptual structure of a mathematical field, so it's a way to see how the flesh is put on the bare bones of the categorical concepts.
It is not critical for register assignment -- in fact, SSA makes register assignment more difficult (see the swap problem; the lost copy problem).
Lifetime analysis is important for register assignment, and SSA can make lifetime analysis easier, but plenty of non-SSA compilers (lower-tier JIT compilers often do not use SSA because SSA is heavyweight) are able to register allocate just fine without it.
Here's a concise explanation of SSA. Regular (imperative) code is hard to optimize because in general statements are not pure -- if a statement has side effects, then it might not preserve the behavior to optimize that statement by, for example:
1. Removing that statement (dead code elimination)
2. Deduplicating that statement (available expressions)
3. Reordering that statement with other statements (hoisting; loop-invariant code motion)
4. Duplicating that statement (can be useful to enable other optimizations)
All of the above optimizations are very important in compilers, and they are much, much easier to implement if you don't have to worry about preserving side effects while manipulating the program.
So the point of SSA is to translate a program into an equivalent program whose statements have as few side effects as possible. The result is often something that looks like a functional program. (See: https://www.cs.princeton.edu/~appel/papers/ssafun.pdf, which is famous in the compilers community.) In fact, if you view basic blocks themselves as a function, phi nodes "declare" the arguments of the basic block, and branches correspond to tailcalling the next basic block with corresponding values. This has motivated basic block arguments in MLIR.
The "combinatorial circuit" metaphor is slightly wrong, because most SSA implementations do need to consider state for loads and stores into arbitrary memory, or arbitrary function calls. Also, it's not easy to model a loop of arbitrary length as a (finite) combinatorial circuit. Given that the author works at an AI accelerator company, I can see why he leaned towards that metaphor, though.
> Given that the author works at an AI accelerator company
I actually believe he works at Buf.build and his resume is stale, the author previously posted about their Go Protobuf parser hyperpb which is a Buf project.
Maybe still "author recently worked at an AI accelerator company" though.
Yes, and??? How is that relevant? We were talking about money, explicitly. Not the resources. When the economy still has room, i.e. people are available to do more, then any missing resources can be obtained by sooner or later by employing people.
This is not about the body or the land, but about the blood or the water flowing through them.
Because it describes exactly the point which GGP tried to make (source: that was me). The assumption is that AI growth is great because without it, look at how low the non-AI growth is! But that argument is flawed because the resources (manpower, materials, manufacturing, energy, ...) absent the AI hypr would not vanish but be used for something else, so the growth in those other areas would be bigger. Granted, perhaps not as big (marginal gains and all that), but the painted picture is still skewed.
WCOJs guarantee an (asymptotic) upper bound on join complexity, but often with the right join plan and appropriate cardinality estimations, you can do much better than WCOJ.
The runtime of WCOJs algorithms are even more dependent on good cardinality estimation. For instance, in VAAT, the main difficulty to find an appropriate variable ordering, which relies on knowledge about cardinalities conditioned on particular variables having particular values. If you have the wrong ordering, you still achieve worst case optimal, but you could have done far better in some cases with other algorithms (e.g. Yannakakis algorithm for acyclic queries). And as far as I know, many DBMSes do not keep track of this type of conditional cardinality, so it is unlikely that existing WCOJ will be faster in practice.
> You can end up writing nearly the exact same code twice because one needs to be async to handle an async function argument, even if the real functionality of the wrapper isn't async.
Sorry for the possibly naive question. If I need to call a synchronous function from an async function, why can't I just call await on the async argument?
def foo(bar: str, baz: int):
# some synchronous work
pass
async def other(bar: Awaitable[str]):
foo(await bar, 0)
Nothing and that’s the problem because even though you can do it, your event loop will block until foo has finished executing, meaning that in this thread no other coroutine will be executed in the meantime (an event loop runs in its own thread. Most of the time there is only the main thread thus a single event loop). This defeats the purpose of concurrent programming.
reply