Hacker Newsnew | past | comments | ask | show | jobs | submit | AndyKelley's commentslogin

flagged for astroturfing

(1) DNS is hard

It's really not.

Just because some systems took something fundamentally simple and wrapped a bunch of unnecessary complexity around it does not make it hard.

At its core, it's an elegant, minimal protocol.


It falls into the category that most people think they understand DNS, the same as JavaScript, or e.g. elections, but the devil is in the detail. And I can tell you, at least for DNS (and Dutch Elections), it's kind of tricky, see fun cases like https://github.com/internetstandards/Internet.nl/issues/1370 and I thought the same before I had my current job which involves quite some tricky DNS stuff (and regarding this we also sometimes encounter bugs in unbound https://github.com/internetstandards/Internet.nl/issues/1803 )

But maybe DNSSEC is the 'unnecessary complexity' for you (I think it's kind of fundamental to secure DNS). Also without DNSSEC they needed RFC's like https://datatracker.ietf.org/doc/html/rfc8020 to clarify fundamentals (same goes for https://datatracker.ietf.org/doc/html/rfc8482 to fix stuff).


Dutch elections? How do they come into this?

There is this list of things tech people think they understand (DNS, javascript), and more common you can see this with everyday people, e.g. with stuff like elections: the basic concept is clear, understandable, but the devil/complexity is in the detail, how to handle certain exceptions. I was employed by the Election Management Body of The Netherlands for a few years, so I can only vouch for the complexity of that relatively simple election system, but I'm pretty sure it will hold for about every country ;)

You and GP are talking about completely different things. Yes DNS at its core it’s an elegant minimal protocol. But all the complexity comes from client side configuration before the protocol is even involved.

We have complexity like different kinds of VPNs, from network-level VPNs to app-based VPNs to MDM-managed VPNs possibly coexisting. We have on-demand VPNs that only start when a particular domain is being visited: yes VPN starting because of DNS. We have user-provided or admin-provided hardcoded responses in /etc/hosts. We have user-specified resolver overrides (for example the user wants to use 8.8.8.8 not ISP resolver). We have multiple sources of network-provided resolvers from RDNSS to DHCPv6 O mode.

It is non-trivial to determine which resolver to even start sending datagrams with that elegant minimal protocol.


Lots of elegant, minimal things are hard to use effectively.

There are steps that three different parties can take, which do not depend on other parties to cooperate:

POSIX can specify a new version of DNS resolution.

libcs can add extensions, allowing applications to detect when they are targeting those systems and use them.

Applications on Linux and Windows can bypass libc.


What about macOS?

they already have CFHostStartInfoResolution / CFHostCancelInfoResolution

What's crazy is that it's almost good. All they had to do was make the next syscall return ECANCELED (already a defined error code!) rather than terminating the thread.

Musl has an undocumented extension that does exactly this: PTHREAD_CANCEL_MASKED passed to pthread_setcancelstate.

It's great and it should be standardized.


That would have been fantastic. My worry is if we standardized it now, a lot of library code would be unexpectedly dealing with ECANCELED from APIs that previously were guaranteed to never fail outside of programmer error, e.g. `pthread_mutex_lock()`.

Looking at some of my shipping code, there's a fair bit that triggers a runtime `assert()` if `pthread_mutex_lock()` fails, as that should never occur outside of a locking bug of my own making.


You can sort of emulate that with pthread_kill and EINTR but you need to control all code that can call interruptable sys calls to correctly return without retry (or longjmp/throw from the signal handler, but then we are back in phtread_cancel territory)

There's a second problem here that musl also solves. If the signal is delivered in between checking for cancelation and the syscall machine code instruction, the interrupt is missed. This can cause a deadlock if the syscall was going to wait indefinitely and the application relies on cancelation for interruption.

Musl solves this problem by inspecting the program counter in the interrupt handler and checking if it falls specifically in that range, and if so, modifying registers such that when it returns from the signal, it returns to instructions that cause ECANCELED to be returned.

Blew my mind when I learned this last month.


Introspection windows from a interrupting context are a neat technique. You can use it to implement “atomic transaction” guarantees for the interruptee as long as you control all potential interrupters. You can also implement “non-interruption” sections and bailout logic.

In particular you need to control the signal handlers. You can't do that easily in a library.

`pthread_cancel()` was meant for interrupting long computations, not I/O.

I completely agree. Huge respect and appreciation to Joran & team.

Thank you Andrew, you always have our full support.

Imagine you have a thriving, successful open source project with many users. Eventually, you want to move on to other things, but you don't want the project to die, or to be absorbed into AWS. Furthermore, you have a bunch of contributors who could really use financial support in order to sustain their efforts. What's your strategy?

I don't know. What are other terminal emulators doing?

It used to be arena-allocated but now it's using a different technique which I outlined in this talk: https://vimeo.com/649009599

> At its core Zig is marketed as a competitor to C, not C++/Rust/etc

What gives you this impression?

I directly created Zig to replace C++. I used C++ before I wrote Zig. I wrote Zig originally in C++. I recently ported Chromaprint from C++ to Zig, with nice performance results. I constantly talk about how batching is superior to RAII.

Everyone loves to parrot this "Zig is to C as Rust is to C++" nonsense. It's some kind of mind virus that spreads despite any factual basis.

I don't mean to disparage you in particular, this is like the 1000th time I've seen this.


You have pretty explicitly framed Zig as a C replacement yourself, e.g.: https://www.youtube.com/watch?v=Gv2I7qTux7g

More broadly, I think the observation tends to get repeated because C and Zig share a certain elegance and simplicity (even if C's elegance has dated). C++ is many things, but it's hardly elegant or simple.

I don't think anyone denies that Zig can be a C++ replacement, but that's hardly unusual, so can many other languages (Rust, Swift, etc). What's noteworthy here is that Zig is almost unique in having the potential to be a genuine C replacement. To its (and your) great credit, I might add.

>> At its core Zig is marketed as a competitor to C, not C++/Rust/etc, which makes me think it's harder to write working code that won't leak or crash than in other languages. Zig embraces manual memory management as well.

@GP: This is not a great take. All four languages are oriented around manual memory management. C++ inherits all of the footguns of C, whereas Zig and Rust try to sand off the rough edges.

Manual memory management is and will always remain necessary. The only reason someone writing JS scripts don't need to worry about managing their memory is because someone has already done that work for them.


Well if anything take as a compliment. As a C, C++ (and some Rust) who lately is enjoying Zig, I think Zig is the only programming language positioned to convince system programming die hard C programmers to use another programming language with simplicity and power backed in.

But completely agree. Its a perfect replacement for C++ and I would say the natural spiritual successor of C.

I gave up using Rust for new projects after seeing the limitations for the kind of software I like to write and have been using Zig instead as it gives me the freedom I need without all the over-complication that languages like C++ and Rust bring to the table.

I think people should first experiment see for themselves and only then comment as I see a lot of misinformation and beliefs more based on marketing than reality.

Thank you very much for your wonderful work!


I got to love that the author of the thing can show up and say “Why?! I never said any of that!”

A lot of stuff related to older languages is lost in the sands of time, but the same thing isn’t true for current ones.


    var group: Io.Group = .init;
    defer group.cancel(io);
If you see this pattern, you are doing structured concurrency.

Same thing with:

    var future = io.async(foo, .{});
    defer future.cancel(io);

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.

> 1. a description of how to carry out I/O that is performed by a separate system

> 2. does the I/O inside the function before returning

How do you distinguish those two things? To put my cards on the table, I believe Haskell does 2, and I think my Haskell effect system Bluefin makes this abundantly clear. (Zig's `Io` seems to correspond to Bluefin's `IOE`.)

There is a persistent myth in the Haskell world (and beyond) that Haskell does 1. In fact I think it's hard to make it a true meaningful statement, but I can probably just about concede it is with a lot of leeway on what it means for I/O to be "performed by a separate system", and even then only in a way that it's also true and meaningful for every other language with a run time system (which is basically all of them).

The need to believe that Haskell does 1 comes from the insistence that Haskell be considered a "pure" language, and the inference that means it doesn't do I/O, and therefore the need that "something else" must do I/O. I just prefer not to call Haskell a "pure" language. Instead I call it "referentially transparent", and the problem vanishes. In Haskell program like

    main :: IO ()
    main = do
       foo
       foo

    foo :: IO ()
    foo = putStrLn "Hello"
I would say that "I/O is done inside `foo` before returning". Simple. No mysteries or contradiction.

https://hackage-content.haskell.org/package/bluefin/docs/Blu...


> I would say that "I/O is done inside `foo` before returning".

It is not. The documentation and the type very clearly shows this:

https://hackage.haskell.org/package/base-4.21.0.0/docs/Prelu...

> A value of type `IO a` is a computation which, when performed, does some I/O before returning a value of type a.

So your function foo does no IO in itself. It returns a "computation" for main to perform. And only main can do this, since the runtime calls main. You can call foo as much as you like, but nothing will be printed until you bind any of the returned IO values.

Comparing it to other languages is a bit misleading since Haskell is lazy. putStrLn isn't even evaluated until the IO value is needed. So even "before returning" is wrong no matter how you choose to define "inside".


I'm also pretty sure that its immaterial if Haskell does 1 or not. This is an implementation detail and not at all important to something being a Monad or not.

My understanding is requiring 1 essentially forces you to think of every Monad as being free.


Ah! My favourite Haskell discussion. So, consider these two programs, the first in Haskell:

    main :: IO ()
    main = do
      foo
      foo

    foo :: IO ()
    foo = putStrLn "Hello"
and the second in Python:

    def main():
      foo()
      foo()

    def foo():
      print("Hello")
For the Python one I'd say "I/O is done inside `foo` before returning". Would you? If not, why not? And if so, what purpose does it serve to not say the same for the Haskell?

My Haskell is rusty enough that I don’t know the proper syntax for it, but you can make a program that calls foo and then throws away / never uses the IO computation. Because Haskell is lazy, “Hello” will never be printed.

You can do this

    main = do
      let x = foo
      putStrLn "foo was never executed"
but you can also do this

    def main():
      x = foo
      print("foo was never executed")
What's the difference?

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

Yes.

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

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.


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)

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.


And for comparison, here's Haskell's (or rather Bluefin's) equivalent of Zig's `Io` parameter:

https://hackage-content.haskell.org/package/bluefin/docs/Blu...


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".

Are you saying "monad" is a synonym of "interface"?

Not a synonym, but `Monad` is one of the commonly used interfaces in Haskell (not the only one).

OK I think I understand now, thank you. My takeaways:

1. Yes, Zig is doing basically the same thing as Haskell

2. No, it's not a monad in Zig because it's an imperative language.


It still is a monad. It's just Zig doesn't have language support for monads, so it's less ergonomic.

Just as modular addition over ints in Zig forms a group, even if Zig has no notion of groups. It's just a property of the construct.

Laziness has nothing to do with it.

What that means practically for Zig, I'm unsure.


Monads do not need to build up a computation. The identity functor is a monad.

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.


A series of functions all passing the same `io: IO` value around exhibit exactly the behavior of the reader monad.

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

Search: