Still, this quote is hilarious:
"Object-oriented programming is an exceptionally bad idea which could only have originated in California." --Edsger Dijkstra
Still, this quote is hilarious:
"Object-oriented programming is an exceptionally bad idea which could only have originated in California." --Edsger Dijkstra
I like this a lot as I run into so many arguments about stateless this or stateless that, and what gets lost is that not contending with state is problematic. Databases are forced to get massive and do all sorts of goofy shit so some people don't have to ever think about state; this goofy shit creates interesting reliability challenges which translates to bad user experiences.
For my future projects, I've decided to build my own application-as-state-database-container thing: http://www.adama-lang.org/docs/why-the-origin-story
Granted, I'm focused on niche board games since they redefine complexity.
Huh? Databases don't do anything "goofy", they implement ACID guarantees.
Which absolutely beats the untold horros of Chtulu proportions that would emerge if programmers had to implement state management at the persistence layer for multiple clients themselves (they'd implement a DB + ACID poorly and in an ad-hoc way, full of holes).
What people seem to forget is that relational databases are not the first technology we had for periststent state. The first versions were custom solutions per app, application-as-state and various ad-hoc such databases with their own formats and techniques, which were such a hellist landscape, devs jumped at relational databases with utter joy...
I think that he meant that functionality that should not is pushed into database. I have definitely seen project full of crazy triggers and sql procedures and so on.
Especially when scale (real or not) is in the picture.
I'm a fan of databases myself, but I am also a fan of document stores as well.
This is true, and it’s the primary reason I’m not finding some way to work in Haskell. But it’s also kind of a cop out.
The issue isn’t just that state exists and will do well beyond our extinction. It’s not even just about how you manipulate state or how many restrictions you put on mutability. It’s also about the visibility of state and stateful processes, how that’s signaled, and the conventions that fall out.
This isn’t something I fully understood until I worked with Clojure. Immutable-by-default was a fantastic constraint. But more valuable to me was that where state is accessed or changed, you can tell immediately.
Just having that gives you several insights:
- This code is more likely to be impacted by outside behavior
- This code has additional concurrency requirements
- This code may be unnecessarily complex
- This code may be hard to understand a week or more later
- This code is documenting its implicit dependencies
Since I’ve moved on from Clojure I’ve mostly worked in TypeScript, and I’ve done my best to apply the same principles.
In some ways it’s a loss: you can’t signal stateful access with @ or ! or *.
But that’s all convention. In other ways it’s a huge win: if you’re trying to write functional code in TS, you eventually end up with the idiom that async/await and Promise types are generally signals of state. And the type system calls that out much more reliably.
All of that said, you do gotta stick the state somewhere. But where and how you do is the difference between eternal pain and eternal much-less-pain. And coupling state with functions on objects is definitely more likely to produce more eternal pain.
Ok, now I'm curious about how StateT fails it.
Good object oriented design goes much, much further. To quote Alan Kay, "Doing encapsulation right is a commitment not just to abstraction of state, but to eliminate state oriented metaphors from programming." (emphasis mine)
I found that it's from this piece, although a brief skim I'm not this piece gives me more guidance about how to do that, but I plan to spend more time with it.
http://worrydream.com/EarlyHistoryOfSmalltalk/
If anyone has other articles to recommend on this concept, please.
Reading it, and learning Pharo, and reading some Smalltalk code, was, for me, one heck of a revelation.
Once I heard that, it makes natural sense that Combine became a first class library in Swift, giving the code base ion channels so that messages just aren’t sprayed everywhere with NotificationCenter or requiring you to name each individual “protein/hormone” message in your program.
once? it's Alan Kay's only speech!
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.
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.
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.
OO says: "State is hard—let's hide it!" (er, sorry, "encapsulate it") FP says: "State is hard—let's isolate it!"
I like F#’s stance on functional-first programming. There are times in which you want to expose the underlying types and there are times you do not. When I recently started a ray tracer implementation, it was a good example of this. The vector, color, point, transform, world, camera, etc. types where all readily implemented by records and discriminated unions which properly isolated but exposed the types. However, for the matrix library, I chose to use F#’s Array.Parallel library and thus a 1D array as the underlying implementation, and this was a perfect use case for using a class. I wanted to hide the implementation of the matrices from the user of the type and encapsulate the internal behavior, only exposing a clean API. Even in that case, the matrix type was immutable because the operations on matrices would simply return new matrices. I think F#’s acceptance of multiple paradigms is really the way to go.
I spend a good part of my time training Java developers away from this idea in order to help them make their code more testable and understandable. Queries and Commands are not just pedantic alternate names for Getters and Setters: thinking about objects with respect to what you can ask it to do (Command or Request) and what you can find out from it (Query) significantly improves the code that gets written.
The real question with all features is "Does it make a given task easier, better, or possible Y/N?" Said question is incredibly vague by design and should differ by situation.
1. Are only conventions. Nobody's forcing you to use them.
2. Simplify refactoring.
3. Are a necessary layer of abstraction any time that 'setting' a value requires updating a related value.
#1 and #2 are only a manner of taste and implementation-specific use cases of tooling built on top of a language. #3, however, is absolutely necessary, to avoid entire categories of business logic errors.
Just imagine if updating a string's value required you to also manually update the string's length. How many string-length related bugs do you think that kind of adventure will result in a typical program?
Arguing against setters is like arguing that
String s = "foo";
must also be accompanied with s.length = 3;
The two operations make zero sense to be done separately - doing so is just a minefield of bugs. The purpose of a setter is to combine similar inseparable operations together.A trivial pass-through setter obviously provides ~zero value, but it also carries with it ~zero cost. Any compiler worth its salt will optimize its invocation away.
You should have a constructor/factory for strings that will form/instantiate the string in a valid state, with its value matching its length. That's not the same thing as a setter that mutates the string in-place.
foo.bar = x; // could be normal assignment or a setter.1) different naming convention between members and props
2) dont expose members; use properties from the start for anything public
to assist with two, they introduce the default get/set implementation, like so:
public object Derp { get; set; }
for when you want to expose an interface with the assignment idiom but dont actually have any interesting implementation
The state is just data structures. You don't need to stick it anywhere aside from some namespace.
You can then have functions operating on those data structures and be fine.
For protection/privacy just make sure access to the data structure instances is not global.