That's true of much of OOP, but implementation inheritance is the elephant in the room - it can't be modeled or understood using a pure FP model, because the combination of late binding and so-called 'open recursion' requires a dispatch step on all virtual method calls (that would in turn be modeled in FP via a "tying the knot" trick). The resulting semantics are extremely tricky, and most practitioners are aware of the problems with them (see "fragile base class").
Some recent programming languages have abandoned implementation inheritance altogether, e.g. Rust. IOW, they're really more like FP-semantics languages with OOP-like syntactic sugar on top.
In the latter cases it’s not inheritance in the sense that one thing derived from another, but that all the things you’d model that way are derived from broadly compatible base types.
Aside: Rich Hickey’s introduction to Clojure made all of my lisp anxieties vanish when he illustrated the syntactic difference as primarily moving a function call’s open bracket before the function name rather than after. It’s silly in hindsight, but it helped me feel more familiar at once and ready to learn the rest.
Un-aside: I think this kind of exercise would be valuable in translating functional-ish OOP (state is isolated but largely operated on with stateless functions) to its FP syntactic equivalent.
For example in Clojure, in Clojure you could syntactically rewrite (map some-hash some-fn) as a map method on a class instance and it’s just moving some punctuation around.
Another one which is probably not very mind blowing here, but did tickle my brain when I recognized it for what it was: before the great concurrency upheavals over the last decade+ (eventually settling on Promises and async/await), Node had monadic Either/Option types as a core part of its interface (you just destructure them as callback arguments).
None of this is meant to disagree with anything you said of course. Just wanted to add some “if you’re approaching state the same way there’s a representation in your environment” flavor to the discussion.
The basic rewrite rules are almost as simple as the λ-calculus; as I remember the notation, it looks like this:
e.m → body[e/i] where m = ςi: body occurs in e
e{m = ςj: c} → {stuff, morestuff, m = ςj: c} where
e was {stuff, m = anything, morestuff} and
there was no definition for m in stuff or morestuff
The expr[replacement/variable] notation implies not only replacement but also α-renaming to avoid variable capture just as in the λ-calculus.You can, for example, define the Boolean values true and false as respectively
{result=ςd: d.iftrue, iftrue = ςe: 37, iffalse = ςe: 5}
{result=ςd: d.iffalse, iftrue = ςe: 7, iffalse = ςe: "yer mom"}
and then if you have some unknown Boolean value b you can compute a conditional as follows: b { iftrue = ςf: "hooray!", ifffalse = ςg: "aww" }.result
Similarly you can define list node prototypes cons { null = ςx: false, car = ςx: 17, cdr = ςy: 72 }
and nil { null = ςx: true }
where true and false are the Booleans given earlier. Then you can define, for example, a length function { result = ςy: y.argument.null {
iftrue = ςw: 0,
iffalse = ςz: 1 + y { argument = ςa: y.argument.cdr }.result
},
argument = nil
}
assuming you have a suitable interpretation of "1 + expression". And, if not, you can rewrite that to something like one.plus { argument = ... }.result, with a Church-numeral-like construction if you're really enthusiastic.I think the ς-calculus is a lot more ergonomic than the λ-calculus in practice, and I've written things like string-parsing code and vector arithmetic libraries in it, or rather in a programming language I implemented called Bicicleta, which is a thin layer of syntactic sugar on top of the ς-calculus, so you can write things like foo(bar, baz) instead of foo { argument1 = ςx: bar, argument2 = ςy: baz }.result and 3 + 4 instead of 3.'+' { argument = 4 }.result. But it's just syntactic sugar.
I'm still not sure if this was a good idea because I'm really skeptical of whether inheritance at all is a good idea. But if it's a bad idea, it's not because it rules out having a pure FP model or even makes it extremely complicated. It's already common to augment the λ-calculus with things like records, arrays, algebraic data types, even generalized algebraic data types, and Haskell-style typeclasses, any of which add a great deal more complexity than the tiny increment in complexity added by using the ς-calculus as a basis.
https://www.quora.com/What-does-Alan-Kay-think-about-inherit...
However, especially (but maybe not only?) in a dynamic language like smalltalk or ruby, you can simulate implementation inheritance pretty closely with just composition and (some kind of automated/macro'd) delegation if you want to. I'm not sure how/if that changes things at a theoretical/formal level.