News for hackers. Stuff that matters.
Re:Exotic Programming Ideas, Part 3: Effect Systems by skybrian on Sunday November 22, 2020 @12:19PM (#25179116)
I expect that, as with any other type system extension, the more granular your effects are, the more likely you are to run into a “what color is my function” problem. If you have a public API that declares certain effects, you’re stuck with those unless you break backward compatibility.

In a practical system, when writing a library and especially an abstract interface, you’d want to be careful what you promise and declare effects that you might need (but currently don’t use), just in case you will need them later.

It’s not that easy even to distinguish functions that can fail from those that can’t, if you’re trying to anticipate how a system will evolve. Something that’s in-memory now might change to being done over the network later.

Re:Exotic Programming Ideas, Part 3: Effect Systems by AaronFriel on Sunday November 22, 2020 @01:48PM (#25179878)
A properly done effects system with type-level annotation of the "color" of functions helps this, rather than hurts as you might surmise.

Take for example logging or tracing - we almost always in a modern backend application want an ambient trace or span ID and a log destination. What we don't want is to have to add those as parameters to _every function_.

So we want to paint these functions with the "logger" and "traced" colors, which respectively allow a function to send messages to an ambient logger via log(message: str) and to get the current trace context via getTraceContext() each with no other arguments.

The compiler will tell us if we call these functions from an unlogged, untraced function, so at a very high level - say at the RPC request level, we paint them early on. In Haskell we might even be able to do something like

    handleReq req = 
      withTraceSpan (...)
      . withLogging (...)
      $ handleReqInner req
Where handleReqInner requires these "colors".

Having too many effect handlers is never a problem either - you can always call an untraced function from a traced function. The "trace" effect handler just falls off at that call. Or in other words: you can always call a function whose colors are a subset of the call-site's.

This is somewhat a foreign concept to people who haven't worked with type systems like this and whose only concept of the color of function is purely binary - either async or not. In a good typed effect system, the compiler will assist you in knowing where you're missing an effect handler, and it will be easy to add those effect handlers sufficiently far from the business logic that you won't have to think about it.

Re:Exotic Programming Ideas, Part 3: Effect Systems by skybrian on Sunday November 22, 2020 @03:44PM (#25180891)
You're saying it's not a problem, but let's say you didn't think ahead and want to add tracing later, and there are many intermediate function calls between top level and the place where you want to trace. You still have to change every function signature to add the colors, right?
Re:Exotic Programming Ideas, Part 3: Effect Systems by AaronFriel on Sunday November 22, 2020 @05:15PM (#25181622)
In a language and type system where the compiler infers all of the intermediate types, no.

The way effect systems are typically added to languages without this level of type system is that the effect "fails". Like making a database call in Python without "with use_database(...):" that provides the database context. In those languages and with those effects, typically the default behavior is the effect's methods are always available, but may either be a no-op/return a nullish value or throw an exception.

As an example, the way tracing works on Node.js is that you have to explicitly declare a span (or use a plugin for a framework which does that for you) and inside that span, you can do something like getSpanId() and it'll return a value. Outside a span, it returns null.

The worst case scenario is you have a language and framework where it's both difficult to annotate the types and difficult to provide some ambient context. In those cases, you're back to (essentially) passing your trace span, your logger, etc as arguments to every function again. Not ideal.

Re:Exotic Programming Ideas, Part 3: Effect Systems by dan-robertson on Sunday November 22, 2020 @05:32PM (#25181747)
In every language I’ve used where types can be inferred, almost everyone writes down the types of their top level functions (or at least the functions that are exposed to clients). I don’t think this is really a good counter argument.

The only ways I know of to reliably make this kind of change (or just about any nontrivial change) to an interface are increasing version numbers and letting clients suffer or having a monorepo and fixing all the clients yourself

Re:Exotic Programming Ideas, Part 3: Effect Systems by AaronFriel on Monday November 23, 2020 @03:08PM (#25191119)
In Haskell, it's the norm I think to do both: let the compiler infer the type and then add the inferred type to the code. This locks in compatibility.
Re:Exotic Programming Ideas, Part 3: Effect Systems by shthrow on Sunday November 22, 2020 @04:09PM (#25181134)
No, think of it like checked exceptions, you only need to handle the effect _at most once_ before it reaches the top (it's up to you to let it keep getting passed up or handle it immediately), otherwise the compiler will automatically infer the effect types in the layers between.
Re:Exotic Programming Ideas, Part 3: Effect Systems by skybrian on Sunday November 22, 2020 @05:09PM (#25181589)
Okay, so type inference is the vital feature here.

It can help in a closed code base. In an open code base, you could inadvertently change a public API, breaking callers that are unknown to you, if the type signature of something public is inferred.

Re:Exotic Programming Ideas, Part 3: Effect Systems by shthrow on Tuesday November 24, 2020 @01:34PM (#25201425)
Correct, but that is the same for any kind public API though. However that is slowly starting to change with statically typed APIs like GraphQL and Protocol Buffers. You can imagine a public API that is introspected and can tell the client about deprecation and all that.
Re:Exotic Programming Ideas, Part 3: Effect Systems by creata on Sunday November 22, 2020 @10:07PM (#25183224)
As Quekid5 said, isn't that the way things should be? Adding tracing is a breaking change, and adding logging inside a deeply nested layer is a breaking change to all the outer layers, too.
Re:Exotic Programming Ideas, Part 3: Effect Systems by Quekid5 on Sunday November 22, 2020 @04:09PM (#25181131)
Yes, you do... which is as it should be. What else would you expect?

If a function needs capability X then it must require that capability. Simple as that.

Re:Exotic Programming Ideas, Part 3: Effect Systems by slaymaker1907 on Sunday November 22, 2020 @08:35PM (#25182768)
Effect systems can actually make this less bad due to functions which are generic over effects. The function color problem is fundamentally because there usually isn't a way to be generic over different colored functions in a performant and ergonomic way.
Re:Exotic Programming Ideas, Part 3: Effect Systems by lambda_obrien on Sunday November 22, 2020 @12:56PM (#25179424)
An effects system like this is more about controlling your own code and allowing for switching off implementations easily versus declaring what effects it has. Your declaration of effects on your function is saying, for example, "I need to output some text," and then in the caller of that function you have to do some action to "consume" that effect. For instance, your example might be an effect called "WriteState" and then you could call that function in a small unit test with a thin layer over a Map, in dev you could call it with a local sqlite db, and in prod you'd call it with your postgres or whatever. Each implementation shares a common interface, but does something different with the data. If you were writing a library, you'd give your public API as the base monad of your library, or as IO maybe, or even give a pure API. You should be dealing with the possible failures under that base context and then the user doesn't need to know about the inner failures.

The effects system effectively acts as an abstraction for some side effect, like an interface, and gets ride of a lot of the boilerplate code needed for mtl or custom transformer stacks.

Also, in strict typing it's pretty easy to refactor with modern linters and such, it actually makes refactoring an API change delightfully simple, just get rid of the red squiggle lines telling you you types are wrong.

Re:Exotic Programming Ideas, Part 3: Effect Systems by skybrian on Sunday November 22, 2020 @03:57PM (#25181022)
Refactoring tools are nice so long as you are in a closed-world environment where you can see all the code and make whatever changes are needed. They don't help nearly as much in an open environment where there are many code owners and not all code is visible to you. When you publish a library, a refactoring tool isn't going to tell you everyone who uses your library, and you don't have permission to change the call sites anyway.

The only thing for it is to push a new, incompatible version and other people will have to migrate. So you're pushing off the work onto them.

I don't see how an effects system helps with this much? It might help you better understand how you painted yourself in a corner, but I don't see how it helps you get out of it.

Re:Exotic Programming Ideas, Part 3: Effect Systems by Quekid5 on Sunday November 22, 2020 @04:13PM (#25181166)
I mean... if you're fixing a broken API, you're going to have to bite the bullet either way -- papering over it isn't going to fix anything... it just hides the problems.

With a type system which understands effects, at least the compiler can give you very accurate help in fixing call sites.

Re:Exotic Programming Ideas, Part 3: Effect Systems by skybrian on Sunday November 22, 2020 @05:31PM (#25181736)
It does help by telling you what's wrong. But in a way, increased precision makes the problem worse.

Suppose you have have a language with two categories of functions, those that can fail (returning an error) and those that can't.

It's nice that within the "functions that can fail" category, you don't have to worry about what kind of error it might be. Error propagation can happen in a generic way. Adding a new kind of error doesn't change any API's.

If instead, you have different kinds of errors and declare them everywhere, you end up with a situation like Java's checked exceptions. The problem is being too precise, which doesn't leave room for changes later.

Similarly, we could be very generic about effects. Maybe we could just say "this function has effects" and treat them all the same? By not being precise about what the effects might be, we aren't promising too much, so we allow ourselves room for change.

The downside is that the caller has to assume any effect is possible. This is a fundamental tradeoff between caller and callee convenience. You need to be specific enough for the caller to be able to deal effectively with the effects you declare, but not too specific, or you're painting yourself in a corner.

Re:Exotic Programming Ideas, Part 3: Effect Systems by lambda_obrien on Sunday November 22, 2020 @08:03PM (#25182576)
Honestly, in the Haskell world, no one really complains about broken APIs, it's actually pretty common for a library to change things in a breaking way. People tend to either use Nix or stack to deal with versioning or they just plan to refactor when they have to. I personally just started with Haskell, but I've switched a small tool from one streaming library to another and it was super easy even though each had a very different API.

Also, breaking API changes happen in every language, on every codebase, ask the time. At least with static types and a common linter I can refactor and be a hundred percent sure it works since it compiles, whereas in Python for example I can't be sure unless I add tests and stuff, even then...

Re:Exotic Programming Ideas, Part 3: Effect Systems by Chris_Newton on Monday November 23, 2020 @01:56PM (#25190227)
Honestly, in the Haskell world, no one really complains about broken APIs

I don’t believe that’s true. The very existence of Stack and Stackage is testament to how significant the compatibility problems had become in the Haskell ecosystem.

Even if it were true, Haskell world is tiny and (in)famously has the motto “Avoid success at all costs”, so it isn’t necessarily a representative example of practices that would be effective in other contexts.

The advantage with a type system as explicit as Haskell’s is that at least if there is a breaking change in an interface you depend on, you get quite good information about where the problem is and what you need to do to fix it.

Also, breaking API changes happen in every language, on every codebase, ask the time.

Some people take backwards compatibility extremely seriously, and code that depends on their interfaces still works just fine many years after it was written. The pace of development is usually slower when robust standardisation is part of the process, but that isn’t necessarily a bad thing given the stability you get in return.

At least with static types and a common linter I can refactor and be a hundred percent sure it works since it compiles

This is a common claim in the Haskell world, but of course it’s not really true. It always reminds me of Knuth’s famous quip, “Beware of bugs in the above code; I have only proved it correct, not tried it.”

Re:Exotic Programming Ideas, Part 3: Effect Systems by skybrian on Sunday November 22, 2020 @09:09PM (#25182929)
Yes, there are easier and harder ways to deal with api migrations. But even in Haskell, isn't "Cabal hell" a thing? And the type of the standard prelude's head function is wrong but it can never be changed.

Dependency migration is the sort of thing that's easy enough in small examples and gets harder as you scale up.

Re:Exotic Programming Ideas, Part 3: Effect Systems by Quekid5 on Friday November 27, 2020 @03:11PM (#25232181)
> But even in Haskell, isn't "Cabal hell" a thing?

No. Just use stack + Stackage and you have a huge set of packages which are mutually compatible.

(You can also use cabal-install with a Stackage snapshot, I think. Not sure though. Cabal-install has also improved massively and doesn't really do "global" installation of dependencies, so it'll help a lot... but you may still run into difficulties making different versions of your project's dependencies work together.)

Re:Exotic Programming Ideas, Part 3: Effect Systems by Chris_Newton on Monday November 23, 2020 @02:01PM (#25190301)
But even in Haskell, isn't "Cabal hell" a thing?

Less so than it used to be. Cabal received a big update a few years ago, and the new-style commands now operate more like many other package managers in terms of isolation etc.

Re:Exotic Programming Ideas, Part 3: Effect Systems by lambda_obrien on Monday November 23, 2020 @02:31AM (#25184367)
You should use Haskell and see what it can do, you can change the prelude easily, for instance.

The standard head function is partial, yes, but that's not a big deal at all.

Re:Exotic Programming Ideas, Part 3: Effect Systems by lalaithion on Sunday November 22, 2020 @03:16PM (#25180661)
The "what color is my function" problem is insurmountable in Javascript, because the runtime does not allow you to call an async function from a non-async one. However, most effects aren't like this. If you say "this function needs randomness" then you can create a pure PRNG and call the function with the PRNG providing randomness. If you say "this function needs logging" you can tell it to log to a string and parse/ignore the string. If it throws an exception, you can catch the exception. Haskell even provides `unsafePerformIO`, which lets you run arbitrary computations as if they were pure.
Re:Exotic Programming Ideas, Part 3: Effect Systems by skybrian on Sunday November 22, 2020 @03:38PM (#25180839)
There is the specific issue with async functions, but that's only one example of a general problem, what I'm calling "function coloring." Workarounds are often possible, but they are still workarounds and often result in bad code.

We've been there with Java. An API takes a Runnable. You need to do something that does IO, so you catch the exception... and then what? Log and suppress it? This is how bad code happens. At best you get chained exceptions.

With Callable, they got smarter and declared it to throw Exception, which means it can only be easily propagated by other methods that declare they throw Exception. So it's a viral declaration, unless you engage in workarounds like exception wrapping.

You don't need an effect system for this to happen, though. In Go, you don't have the problem with different kinds of errors, but you can still classify functions into two categories: those that may return an error, and those that (apparently) always succeed. If you need to fix the API, you might end up with lots of refactoring [1].

Mistakes at the API level can be hard to fix without touching lots of API's. This is unsolvable in general. The only way to avoid workarounds (via escape hatches or by mocking things out) is to standardize whatever you can so it works together.

[1] https://www.dolthub.com/blog/2020-11-16-panics-to-errors/

Re:Exotic Programming Ideas, Part 3: Effect Systems by lalaithion on Sunday November 22, 2020 @04:48PM (#25181408)
Nothing prevents having a type system capable of dealing with those problems. Yes, Java and Golang make this difficult, but if you have a language that supports it, there's nothing which prevents writing an API that says "Whatever the effects of the Callable you passed me are, I also perform those effects".
Re:Exotic Programming Ideas, Part 3: Effect Systems by skybrian on Sunday November 22, 2020 @05:17PM (#25181642)
Introducing generic types doesn't make the function-coloring problem go away.

Now, instead of having a single function that has some effects, you have a family of closely-related functions, each with different effects. These functions are incompatible with each other. The function-coloring problem has gotten worse! :)

(Generic types are still useful though.)

Re:Exotic Programming Ideas, Part 3: Effect Systems by lalaithion on Monday November 23, 2020 @12:48PM (#25189422)
No, it doesn't make it go away, but it makes the "function coloring problem" about as much a problem as the "colors" of functions as determined by their return types. You can't pass a `func () string` to an api accepting a `func () int`.
Re:Exotic Programming Ideas, Part 3: Effect Systems by karlicoss on Sunday November 22, 2020 @08:45PM (#25182825)
And indeed, JS Flow detects such async issues pretty well
Re:Exotic Programming Ideas, Part 3: Effect Systems by no_wizard on Sunday November 22, 2020 @05:57PM (#25181917)
I think the reactive/rx/observer patter might be an acceptable solution around the what color is my function problem and it tends to be very popular with the functional programming wing of the JS community from what I’ve seen.

Never understood why the browser didn’t ship a full access to something like an EventEmitter. Since you can dispatch events on window document DOM elements etc seems like being able to subscribe to events in a more arbitrary fashion that worked in parallel to the other events would have been useful and solved callback hell all the same

Re:Exotic Programming Ideas, Part 3: Effect Systems by the_benno on Monday November 23, 2020 @04:47PM (#25192332)
We built an effect system to enforce thread safety in Rx-based android apps a few years ago and published a paper on it at ASE, if you're interested: plv.colorado.edu/benno/ase18.pdf

the idea should generalize to whatever thread-pool/effect constraints you want to express, but the paper and implementation are focused on keeping UI effects on the android main thread.

Re:Exotic Programming Ideas, Part 3: Effect Systems by jiggawatts on Sunday November 22, 2020 @11:51PM (#25183694)
A lot of people are saying that function coloring is essentially an unsolved problem, and this is true. However, coloring of individual functions is not strictly speaking necessary.

Think of the classic "zlib" style C libraries. Often they can't assume anything about the target platform, even basic stuff like memory allocation or I/O, because there's such a huge variety compilers, standard libraries, and restrictions.

So just about every such library exports an "API context" struct, full of function pointers. These then allow the caller to pass in implementations for malloc, free, threading, I/O, etc...

The trick is that these can have default implementations. So the function pointer in the context for allocation can simply point to "malloc()". This can of course be overriden, so the capability is there, but the caller need not do anything by default.

Features like logging and tracing can be similarly sent through to default implementations that do nothing or call standard APIs in some naive (but sufficient) way.

Re:Exotic Programming Ideas, Part 3: Effect Systems by ledauphin on Monday November 23, 2020 @08:18PM (#25194119)
I think this (allowing some form of default for backwards compatibility) is a underrated point.

The common modern example being async functions, I would say it's actually quite surprising that JavaScript can't seem to provide the sane default behavior of "block this non-async function until the async function it calls returns a value".

The reason it can't is simply that it chooses not to provide a runtime with a true threading model - blocking a non-async function means literally blocking the entire runtime. But there's a possible world in which a "colored" (async) function could be used directly in a synchronous function using an escape hatch provided by the runtime, and then async functions would no longer be colored in the same way.

Re:Exotic Programming Ideas, Part 3: Effect Systems by jahewson on Sunday November 22, 2020 @01:20PM (#25179596)
I always found IOException in Java to suffer from this problem.
Re:Exotic Programming Ideas, Part 3: Effect Systems by codebje on Monday November 23, 2020 @02:07AM (#25184250)
Java's checked exceptions aren't (usefully) polymorphic. You can't write, for example:

    R process<R, E extends Exception>(ThrowingFunction<Long, R, E> processor) throws E { ... }
You could write "process" to the Exception base class, but it still can't handle a "processor" that doesn't throw at all.

Checked exceptions a "bolted on" effect that sits outside the usual type system in Java, and their presence on a method signature "colours" it in a way that an effect described inside a type system would not. More of a stain than a tint, if you will.

Java 8 resolved it by quietly pretending checked exceptions never happened, which works fine until (1) you get unhandled unchecked exceptions in all their glory at runtime, and (2) you want to use one of the Java 7 or earlier methods as a "processor" but, oh no, it throws an exception.

(It's a deeper colouring than JavaScript's async/await keyword, which is just syntactic sugar around continuations in callbacks and returning promises - you can pass an async "processor" function to a "process" function that has no idea what it's getting and get back the promise you'd expect from it.)

Re:Exotic Programming Ideas, Part 3: Effect Systems by The_rationalist on Monday November 23, 2020 @08:03PM (#25194000)
Arrow Fx manage to be in a way colorless by obscoleting the need of Applicative, monad, etc https://arrow-kt.io/docs/fx/polymorphism/