@wolf480pl I was thinking of doing a "hyper-pure functional language" experiment that would work something like that. Having strict pass-by-value semantics (no pointers, at all), and a main function that returns it's arguments for processing in the language runtime, which is the only place that can interface with the world would naturally lead to libraries explicitly requiring objects representing external state to them.
@wolf480pl Not quite, I don't think. Tho I'm not well versed in traditional functional languages so might be wrong. this is what I'm thinking off.
@wolf480pl none. Python is the one I know best, but that's not functional, C is far from functional either, others I don't know well enough to know how they work.
Hmm ok I guess I'll have to explain it from scratch. Would you prefer if I borrowed C++/Java's (List<T>) or Python's (List[T]) notation for generics, or just go with Haskell's (List t) ?
@wolf480pl Any but haskell is fine. Do note that I do have a fair bit of knowledge of types, but not the languages that use them, so no need to ELI5.
@ignaloidas ok, so let's go with List<T>.
I'll be using arrow for functions though, so instead of:
first : Function<List<T>, T>
first : List<T> -> T
consider the following type:
type State<S><R> = S -> (R, S)
a function that takes state or type S, and returns a return value of type R, as well as a new state of type S
you can define some nice operators on it that'll make it possible to chain it nicely (then we'll call it a monad), but I'll elaborate on that later.
So what you're saying is, main is a State<RealWorld><int>,
and RealWorld is some non-opaque thing that consists of stdin, stdout, and other things?
@wolf480pl Yeah, somewhat, just with RealWorld being defined by main itself (no predefined structure).
@wolf480pl no. It's run again and again until a sentinel value (not pictured in the diagram) is set to some exit code.
@ignaloidas even more interesting...
and because main is pure, it will never block...
I think you'd need some syntactic sugar for it to be able to not have a huge switch(state) at the start, or sth like Python's generators...
how do you read from regular files though?
Do you somehow return a request to read?
@ignaloidas btw. virtualization would be so easy with this API, even easier than with lua coroutines
@wolf480pl it's quite naturally building a state machine, so having a good switch syntax is a requirement either way. Of course having a large switch is still annoying, but I'd imagine this approach would be more fitting for server applications anyways, where besides startup you don't have many different states.
as for files, you'd just have file objects returned to runtime with appropriate fields written, and the runtime returns them back with what's needed.
@ignaloidas sometimes it's natural to keep state in the (metaphorical) "instruction pointer", especially when you have some form of async io.
Eg. it'd be more convenient to write an entire handshake with a new client as a single linear piece of code than breaking it up into steps.
Anyway, so your idea boils down to main returning "syscalls" it wants to do, and then being called back with the results?
It's pretty cool IMO, and would lend itself well to all kinds of things:
the runtime could be:
- a minimal wrapper that sends everything over a ring buffer to a real runtime on a different CPU (spectre mitigation, heterogenous virtualization, etc)
- a minimal wrapper that sends everything over a socket to a real runtime on a different machine
- a different program whose main() is the real main, and it's only pretending to be the runtime to you
- an async io implementation that allows you to wait on multiple syscalls in parallel w/o changing code
@ignaloidas it seems so good I can't believe nobody tried it before, but I'm afraid it could've been nodejs and nodejs is shit so...
@wolf480pl It's not without it's problems. The runtime may submit a ton of syscalls at the same time if your main requires it, which can put too much load on system resources, whereas that instantaneous load on system resources could be distributed over a larger time to avoid congestion. If the main processes many requests at the same execution, a single request that takes a while will tank latency figures for all the others.
There's probably a few more footguns I haven't thought of as well.
@wolf480pl oh, and for why nobody tried this before, I believe a reason for that could be the fact that this only really makes sense for functional languages, and from my cursory look, most functional language people seem to be allergic to the idea of a runtime.
@wolf480pl GC isn't the same as a runtime. Golang has a GC and I don't think it has anything more that could be called a proper runtime.
Consider libc+ld.so. It:
- provides some utilities
- calls your code from __libc_start_main
- lazily resolves calls to dynamically linked functions
Is this a runtime?
@wolf480pl Yes, I do believe that's a runtime. From a cursory look I also think that most functional languages try to avoid having something like that.
@wolf480pl oh, huh, now that's a runtime, not sure how I missed it.
I guess a better distinction could be that most functional languages aren't designed with a runtime in mind.
The social network of the future: No ads, no corporate surveillance, ethical design, and decentralization! Own your data with Mastodon!