As far as I know, one can create a denotational semantics for any well-defined language. In practical terms, you could pick, say, JavaScript and do a similar representation as explained in the article: Every “non-pure function” is evaluated to a description of the state change that is caused by that function.
What is different for approaches as mentioned in the article is that all denotations (what we represent the result of applying a possibly effectful function) of the formal semantics, are also values in the language itself.
What this gains us is hinted at in the article: We can now implement different ways to execute these effects (i.e., for different execution environments or testing) »as a library« (without modifying the language interpreter/compiler).
However, this flexibility seems to be primarily used by language/framework implementers themselves. Not by application developers.
Library implementations of “effect systems” seem to create their own DSL for denotations (i.e., having their own IO type, maybe reusing some closeby syntax from the host language).
For application developers, in case of this article, it seems that the main improvement is that effectful computations are marked with an ! so they are easier to spot.
Don’t get me wrong, I think that is very valuable for very little additional syntactic complexity, so it’s a great thing to do.
However, the more I look into this the less I buy into the whole “functional semantics” for side-effects argument. Immutable data is great, explicit dependencies are great, but those are about minimizing the use of side-effects, not about dressing them up.
> However, this flexibility seems to be primarily used by language/framework implementers themselves. Not by application developers.
In the case of Roc this is determined by the platform and the platform corresponds to the framework level. A webserver platform is going to have a different set of execution choices than a cli platform or a platform for defining user functions in a db. So I wouldn't expect every application developer to make a platform but it's not going to be a compiler/stdlib only sort of thing.
As for the fractured IO, I'm pretty sure that every single person who has dealt with any functional language is aware of the propensity towards fractured IO ecosystems. The exact details of sharing code across different platforms in a useful way is somewhat hand waved at the moment in that there's a rough, unimplemented plan. I kind of expect that there's going to be some piggybacking on the WASM interface definitions but there are a couple changes around Tasks and the module system that are in the pipeline before that'd come up.
> As far as I know, one can create a denotational semantics for any well-defined language. In practical terms, you could pick, say, JavaScript and do a similar representation as explained in the article: Every “non-pure function” is evaluated to a description of the state change that is caused by that function.
Yes this argument has been done before [1], so it's important not to lose sight of the real value proposition:
I want the ability to write a function which returns the same output for the same input, regardless of when/where it's called, number of threads, whether it's in a test or in prod, and so on. But I can already do that in an any programming language, so the real value here is being able to mark the function as such, and have the compiler reject me if I'm wrong.
> However, the more I look into this the less I buy into the whole “functional semantics” for side-effects argument. Immutable data is great, explicit dependencies are great, but those are about minimizing the use of side-effects, not about dressing them up.
A function with minimal side-effects is a function with side-effects [2].
> effectful computations are marked with an ! so they are easier to spot
There's two important steps that you've rolled into one. The first step is the 'functional semantics (with explicit effects)'. E.g. you take your straight-line imperative program:
html = Http.getUtf8(url);
path = Path.fromStr(filename);
File.writeUtf8(path, html);
Stdout.line ("Wrote HTML to: $(filename)");
and model it monadically:
Http.getUtf8(url) >>= \html ->
path = Path.fromStr(filename)
File.writeUtf8(path, html) >>= \_ ->
Stdout.line ("Wrote HTML to: $(filename)");
This is where you get your nice functional properties meaning that your compiler won't let you mix them up. Detractors call them red/blue functions. The second step is wrapping it back up into a palatable syntax again.
Haskell and Scala use <- for this. Idris and Roc use !. Js uses await.
Yes, Conal Elliots arguments are very relevant here.
> A function with minimal side-effects
Clarification, what I mean with “minimizing the use of side effects” is not about within a function, but in a program.
Exaggerated example: You could write Haskell in a style where everything returns an IO and mutates some global state.
Not doing that, and having most things pure is useful due to the simplified reasoning in the pure parts.
> There's two important steps that you've rolled into one.
Have I?
What I want to say is that I do believe the syntactic distinction between calling side-effecting functions vs pure functions is useful.
Do I need to care about how the compiler enforces that? Does the specific encoding need to be part of the language semantics? Do I need to be able to model it monadically within the language?
> This is where you get your nice functional properties meaning that your compiler won't let you mix them up.
I may have formulated this a bit flippantly, but I think you mean the same here as I did when I said that “marked with an ! so they are easier to spot” I did have in mind that this is checked by the compiler.
And I think we also agree that this separation is a good thing.
> A function with minimal side-effects is a function with side-effects [2].
Perhaps the real issue is being able to categorize those side-effects at design-time and control behavior based on category.
In particular, logging and telemetry which (ideally) do not matter to the core behavior of the application, versus web-calls (which probably do) and local memory changes (which almost always matter.)
"However, this flexibility seems to be primarily used by language/framework implementers themselves. Not by application developers."
Yes, but the question is whether that is because it isn't any good for application developers or because application developers are unaware that it is an extremely useful idea, and if done properly, doesn't even carry that much marginal cost.
I work primarily in imperative languages, like most people, but almost all the code I write now I separate into an "IO driver" and relatively pure code that uses the driver as a parameter. Like, everywhere, in every language I use now. The proximal reason is that testability skyrockets when you do this, because what makes tests hard is typically the intermixing of these two concerns. When you isolate them they are almost always vastly easier to test in isolation from each other, and this even contributes to integration level testing higher up.
I say it isn't that expensive because once you get over an initial period of practice and learning how to do this, the difference between (in some random psuedocode)
func DoUserInput():
x = userInputLine()
y = bigPureComputation(x)
outputResult(y)
and something like
interface IO:
inputLine() string
outputResult(string)
func DoUserInput(IO IO):
x = IO.inputLine()
y = bigPureComputation(x)
IO.outputResult()
type RealIO:
RealIO.inputLine = inputLine // the global version here
RealIO.outputResult = outputResult
really isn't that great; here you see all the overhead front and center but at scale this tends to be less of a big deal on a percentage basis than this would imply, and it pays off immediately in testability... yes, what I wrote implies that bigPureComputation must be easy to test, but now I can provide drivers that allow me to essentially push that purity up into DoUserInput and test it as if it were a pure function, because when I combine that with a testing implementation of IO the whole DoUserInput+TestIO essentially does become externally pure, even if you can look on the inside and see things like input being consumed and such. Testing RealIO is also easier because now it's all broken into its component pieces, and of course at real code scale all of these little snippets can expand into arbitrarily complicated and otherwise difficult-to-test functionality.
In my toolbelt, this technique is basic, fundamental, and desperately underutilized by application developers. It is sooo easy to write code up front in this style and such an amazing pain to refactor into it after the fact. Doable, absolutely, but an amazing pain.
In the context of my argument its quite interesting because you essentially argue that one can get the benefits of separate runtime implementations without actually needing pure functions (in a very strict sense) or any of the IO encodings.
What I had in mind when I said application specific handling, I meant things like the application developer writing a custom optimization pass on the returned IO values to make the execution more efficient for the specific application. Or adding custom way of handling/reporting errors.
And I meant doing so while not making their own interfaces and instead working with whatever the common IO abstraction is.
As far as I know, one can create a denotational semantics for any well-defined language. In practical terms, you could pick, say, JavaScript and do a similar representation as explained in the article: Every “non-pure function” is evaluated to a description of the state change that is caused by that function.
What is different for approaches as mentioned in the article is that all denotations (what we represent the result of applying a possibly effectful function) of the formal semantics, are also values in the language itself.
What this gains us is hinted at in the article: We can now implement different ways to execute these effects (i.e., for different execution environments or testing) »as a library« (without modifying the language interpreter/compiler).
However, this flexibility seems to be primarily used by language/framework implementers themselves. Not by application developers. Library implementations of “effect systems” seem to create their own DSL for denotations (i.e., having their own IO type, maybe reusing some closeby syntax from the host language).
For application developers, in case of this article, it seems that the main improvement is that effectful computations are marked with an ! so they are easier to spot. Don’t get me wrong, I think that is very valuable for very little additional syntactic complexity, so it’s a great thing to do.
However, the more I look into this the less I buy into the whole “functional semantics” for side-effects argument. Immutable data is great, explicit dependencies are great, but those are about minimizing the use of side-effects, not about dressing them up.