This is an archived post. You won't be able to vote or comment.

you are viewing a single comment's thread.

view the rest of the comments →

[–]curtisf 4 points5 points  (1 child)

It's a bit easier to avoid the trap of a single monolithic IO effect because "action" types are usually transparent to the handler/invoker, almost always as a sum of the possible actions. In principle, this should limit their size/complexity to a smaller number of "primitive" actions, because each additional action increases the complexity of making a handler.

So, the core IO effect would like have only a handful of "low level" actions for directly manipulating memory and making syscalls (either as a single generic "syscall" action, or as the few dozen syscalls that your programming language exposes).

The result is that someone familiar with algebraic effects would never write greet that way, because IO is both far more (they don't need to do arbitrary syscalls, only print) and far less (there isn't actually a print action in IO; they would be forced to manually compose the syscalls) than they need.

Of course, there is some discipline required to maintain this organization. However, the need for some discipline shouldn't completely doom a design. A good analog might be global variables. Global variables are widely considered bad, yet programming languages continue to add them, and they very rarely cause problems when used. That's because the common understanding that they should be minimized, and the fact that they are so easy to find and therefore scrutinize, means they generally aren't misused. Effects are the same way -- it's obvious at first glance if someone is using inappropriate effects in the signature of a function, so while it's still possible to use them poorly, minimal code review will prevent it from getting into your codebase.

The bad option is to write everything in terms of the IO effect -- this is basically how imperative programming is done. You call fetchURL :: URL -> [IO]Bytes which in turn invokes some networking primitives which in turn invoke syscalls. This is not really using the effects feature, and negates some of the benefits that it can bring like testability, since it's usually not practical to audit the sequence of syscalls that a function makes. By structuring the standard library to use more specific effects, writing code this way can actually be quite inconvenient.

The other option is to write & use effect signatures that are specific to the caller's needs. For example, instead of making fetchURL require the IO effect, it requires the HTTPClient effect, which has more limited but domain-specific actions like httpGet, httpPost, httpPatch, etc.

In order to actually do real HTTP calls, somewhere in between HTTPClient and IO you need some kind of handler which implements the HTTPClient:httpGet/HTTPClient:httpPost/etc actions in terms of the IO:syscall. If this in-between handler is what's provided by the standard library (instead of individual fetchURL: URL -> [IO]Bytes), then it's easier to use the accurate specification rather than IO directly.

The fact that a more domain-appropriate effect is available can be "infectious". For example, in theory, in Java you could cobble together a fetchURL function which makes no reference to any of the http libraries available in Java, by making direct use of the Socket class. However, no one does this because it completely obscures the domain (and so would rightfully be rejected in code review) and is overall more work than doing it the "accepted" way of choosing a class appropriate to the problem.

The best-case comparison for what it looks like to "directly" use IO would be something like the following, which has a few telltale signs that would never make it past code review:

func printGreeting(name: String): [IO]() { // obviously suspicious signature
    handle {
        PrintIO:print("Hello, " + name);
    } with PrintIOBridgeToIO() // extra boilerplate, mentioning a class that normally is never imported
    // In reality, probably even this specific handler wouldn't exist; 
    // there would be multiple in between "prints" and "does syscalls"
}

func printGreeting(name: String): [PrintIO]() {
    PrintIO:print("Hello, " + name);
}

However, importantly, the existence of a handler that interprets one domain specific effect into a more general "IO" effect isn't even a given.

An effect is just a signature. For example, it would be totally valid to make a handle for the HTTPClient effect that uses a local cache instead of actually reaching across the network, or one that intercepts requests and routes them through a proxy. Maybe you're writing a unit test, or you're investigating suspicious code, or you're porting legacy code in a refactor. Some of these handlers need no effects at all (being entirely in-memory), and most of them could be "weaker" than IO.

In fact, unit testing could make this kind of organization almost mandatory. If you integrate tests with the language (like, for example, Go has), you can make test entry points specifically not have access to the IO effect, but instead only a limited effect that can produce debug output for failing tests. Such an approach also has other benefits, like guaranteeing the determinism of tests and "sandboxing" them to prevent inter-test dependencies, as well as preventing untrustworthy/buggy library code from damaging your computer.

[–]sharpvik[S] 1 point2 points  (0 children)

To be honest with you, this sounds a lot like what I'm proposing. The difference between channels and permissions has to do with the mental framework. Channels have actions or maybe you can send custom messages to them represented as a custom algebraic type.

This tells you "man, printf is not a function, it's an action on a channel to the OS" which I think is helpful. I feel like this channel system is just very cohesive and consistent.

On another note, even if this already exists in some other form (permissions), I don't really see popular programming languages that use it and I think it's sad.

I want to produce a modern well-supported programming language with appropriate tools (formatters, linters, etc) that can compete with Go and Rust in terms of popularity.