Hacker News new | ask | show | jobs
by mpweiher 1344 days ago
No.

Applications are not functions.

How many applications that you use on a daily basis work as follows:

1. You prepare some parameters

2. You start the application with those parameters.

3. The application goes away and thinks for a bit.

4. The application returns with a result and then exits.

Trying to make actual applications and system fit into the function (or procedure) mold is, IMHO, one of the biggest obstacles to software simplicity, as there is a fundamental architectural mismatch here.

In the early days of computing, a lot of programs actually did work this way, which is why DSLs for algorithms (ALGOL) were appropriate, and probably where the idea originated that they are actually general purpose languages. Which they are not.

8 comments

Pretty much every web application backend is like this? For efficiency it skips recreating the entire OS process, but serving a single web request is exactly preparing some parameters, the app thinking for a bit and then coming back with a response.

Web app abstraction frameworks like Rack (Ruby), WAI (Haskell) and many others work exactly like that: they allow you to supply them with a function taking a HTTP request and returning a HTTP response, then run that for each incoming request. Nothing magical about that and it works extremely well in practice.

You're skipping the part where Rails takes that Rack simplicity and does a whole lot of magic. There's a huge amount of state contained within Rails and then even more in the DB.

So yeah, we can say web servers work like functions if we cut out all the bits that don't and call them separate applications. But realistically they're not.

Well of course, more complex applications are going to have more complex state. Rails does a lot more than most web frameworks, so it has a lot of extra state to keep track of. This doesn't make it any less a function, it's now just a function that takes in the internal state (including the db contents) as an input. That's exactly what the article describes.
> just a function that takes in the internal state (including the db contents) as an input

No Rails (or other) app has a function like this where the entire contents of the DB are passed in as a parameter.

And you can't handwave complex internal state as simply another parameter to a function because that complex state can change. That makes the function no longer solely dependent on the parameters passed in. Its behavior is dependent on the parameters passed in plus any changes made by other processes to the complex internal state.

Maybe I've been spending too much time with the state monad, but that still sounds like a function to me. The fact that the DB contents get passed in implicitly as part of the context of a Rails app does not change that.
There's a difference between a "function" that uses implicit contextual state and one that doesn't. We can call it a function with implicit context or we can call it an object or a service or a process. It doesn't really matter as long as we have some way of talking about it. Those other words have been in common use for a long time so it's probably more convenient to use them.
That's exactly my point.

Sure, it is possible to model these things as functions, for example by "passing" the entire world as a "parameter". However, it is an extremely contrived modelling, basically you're hammering the square problem domain into the round function hole with the biggest hammer you can find until it "fits".

It is also possible to model these things as Turing machines, or NAND gates...

The "app-as-function" is not at all contrived though, it has some very concrete applications that make use of the composability advantages that functions have. The [rack-test](https://github.com/rack/rack-test) gem (and similar languages) work exactly like this: they run the "app" part without the "web server" part to enable much easier testing. Rack middlewares similarly treat the enclosed app as just a function taking some specified input and returning some output value.
Your numbered list seems like an appeal from incredulity based on the title rather than an interaction with the article.

TFA basically describes Elm’s architecture https://guide.elm-lang.org/architecture/ which is one of the state of the art abstractions for building applications where you model state changes in a central function.

Or watch Gary Bernhardt's "Functional core, imperative shell" talk (https://www.destroyallsoftware.com/screencasts/catalog/funct...) that compelled TFA.

I am familiar with both of these, as well as the article, thanks.

Here is a quote from the article:

Fundamentally an application can be written to have at it’s core, a single, stateless pure function, behold!

And my point is that, yes, you can do that, but you shouldn't.

But your bulleted list is an incorrect entailment of what "a function at its core" means wrt OP, Elm, or Bernhardt's talk, so it seems more like you're reacting to your own reductio of what that quote could entail.

You haven't explained what is wrong with using a pure function to model state changes in an application. To do so, I think you'd also have to contend with the fact that a solution like Elm works quite well in practice.

Why not?
Your point is not particularly convincing.
Hey, thanks for your input.

I blame myself for this misunderstanding as I wrote the article. I certainly am not suggesting that an application is a function, rather an application can be represented by single function as its core.

There is an important distinction here as the production applications I have utilised this architecture in certainly do not literally behave as functions to the external user in the manner you describe, the user interaction is in no way affected by the architectural choice.

As others have eluded to this core pure function is really just a boundary between the external side-effect laden world (e.g. UI / network / sensors / OS etc) and the applications business / domain logic and NOT the boundary between the OS and the application which seems to be what you are describing.

I appreciate your input and the discussion it is provoking.

No misunderstanding on my part, no worries.

And apologies for the misunderstanding due to my terseness, which is partly due to me having written about this in great depth on a number of occasions, here are two examples:

- Why Architecture-Oriented Programming Matters[1]

- Can Programmers Escape the Gentle Tyranny of call/return?[2]

The basic point is that we are so used to programming simply being call/return (and functional is a subset of that), that it is very hard for us to conceive of programming being anything else.

And one consequence of that is that we try to map every problem onto a call/return (incl. FP) solution, no matter how inappropriate the mapping, and the mapping very often is inappropriate, leading to architectural mismatch. [3][4]

We do this partly because we really don't know better, but also partly because there are tangible benefits, primarily that once we have done that mapping, we can then express whatever we arrived at pretty directly in our languages, because our languages effectively only allow call/return based abstraction.

It's a bit of a conundrum.

[1] https://blog.metaobject.com/2019/02/why-architecture-oriente...

[2] https://2020.programming-conference.org/details/salon-2020-p...

[3] https://repository.upenn.edu/library_papers/68/

[4] https://rd.springer.com/content/pdf/10.1007/978-3-540-92698-...

But this is the problem with basically every functional programming article that makes it's way to HN. As a thought experiment it makes sense but it's not transferrable to the real world.

Like take the log out action as an example. Mid request we are going to have to change the state from a user logged in to a user logged out. Otherwise when we render the home screen, as an example, they'll see the logged in version instead of the anonymous user one.

And realistically we want need a transition state to show a successfully logged out message since we do not want that message to show the next time they come. So the whole sequence for this simple action is:

Old State -> Intermediate State -> Render Response -> New State for the next request.

So we have to change state a couple times and pass that along.

And then how do we deal with stuff that's actually state. Like a database or whatever. Are we really passing that as a function parameter so that 10 calls deep can use it?

And what happens 6 months in when we want a caching layer? Do we go back and edit every single function to accept a Redis argument now?

Realistically, no. We have some sort of config or server object that we pass along. So we have an object that contains all of our server state that gets passed to most functions. How is that better than a single global variable holding that state accessible everywhere in the application?

Thanks for your input

> ... as a thought experiment it makes sense but it's not transferrable to the real world.

For the record, it's certainly not a thought experiment, I have been using this in the real world for a few years.

> Like take the log out action as an example...

The problem you outline with regards to intermediate UI states around the logout flow example is one of assumed responsibilities. If an application needs to show some specific transient UI when the application has moved between two states well, that's a UI concern and does not have to be modelled as part of the core application state. For example if you have a UI for an application which is required to show some jazzy animation when a user transitions from logged-in to logged-out that in no way needs to be part of the core application logic. The UI can just inspect the incoming state and conditionally when detecting a move from LoggedIn to LoggedOut trigger some UI animation / transient dialog / whatever.

One nice thought experiment / thought pump I find of value when dividing core application (~domain) and UI responsibilities is thinking about if you were to switch the UI from say a mobile framework (say Android/Compose) to a desktop terminal, what stuff stays the same and what stuff is display specific. In the above example you most likely would not want to show a jazzy animation on the terminal and therefore is display specific and should no be modelled in the core application logic. This is a nice simple thought process for dividing these responsibilities.

> And then how do we deal with stuff that's actually state. Like a database or whatever. Are we really passing that as a function parameter so that 10 calls deep can use it?

No, a database would fall under IO and would make more sense to access behind a Command interface in this pattern, like most other side-effect interactions. This fits into the "functional core imperative shell" mindset referred to in the post.

> And what happens 6 months in when we want a caching layer? Do we go back and edit every single function to accept a Redis argument now? Realistically...

I am in no way experienced with BE caching layers, my experience is mostly client side mobile applications. However, my first approach for an in-memory DB cache would be to chuck that in front of the DB access behind the same abstraction, and also this would be behind the Command interface, again like all other side-effect interactions.

> How many applications that you use on a daily basis work as follows…

It is actually my full time job to support a suite of applications that do exactly this.

Yep, and these applications certainly do still exist. Scientific applications come to mind, probably also engineering support, and maybe financial modelling?

However, whereas they used to make up the overwhelming majority of programs, they are now minority, yet we still try to model all programs like this.

Which is obviously possible, after all we're doing it. But it's not a good idea.

Most application / API servers do pretty much that, they only to mot terminate the OS process.

Moat interactive applications can be seen similarly, they just do that in a loop, for every event coming from the user.

> How many applications that you use on a daily basis work as follows...

Most of them, but then I use the CLI a lot. E.g.:

    find <params> | grep  <params> | xargs foo <params>
Where "foo" is (of course) a kind of meta-parameter, both a parameter itself and a function/application.

- - - -

How do you mean, ALGOL isn't a general purpose language?

1. The shell is not calling functions, it is composing filters via I/O. Superficially similar, but actually quite different. (And how it makes it superficially look like function application is also quite interesting, IMHO).

2. But yes, programs that act like functions definitely do exist. They even used to be in the majority.

3. ALGOL stands for ALGOrithmic Language. It's a DSL for the domain of algorithms. As are virtually all our mainstream so-called "general purpose" languages.

In re: #1 that's the kind of discussion I could have all day. (If you take the state of the machine including the filesystem, then almost all non-networked programs could be treated as pure functions, eh?)

#2 so how does that modify your original comment, if at all?

#3 What would you contrast our mainstream so-called "general purpose" languages with?

Well you just described every report ever.
Did you read the post or just the title? It doesn't say that applications are functions.
TFA:

Fundamentally an application can be written to have at it’s core, a single, stateless pure function, behold!

Yes, "at its core" being the center of the post. Are you ready to read it now?