But he eventually turned to the dark side. He claims clojure, a functional lisp, is his absolute favorite language.
He’s even got blog posts about it! Multiple!
I stumbled on this while reading about clojure. I really like his blog!
https://blog.cleancoder.com/uncle-bob/2019/08/22/WhyClojure....
Previously, I thought FP was a way to happy-path incidental habits to avoid studying every pattern. But if patterns are discovered, arise out of independently invented idioms, then the best I could do is reinvent what everyone else has found worked for them (and turned out to be a design pattern).
It has also helped me look at Gang of Four (GoF) examples less literally--if we don't have exactly these classes, it's wrong--to context matching a potential solution with a given problem.
The light bulb moment is when OS artifacts like filesystem, programming constructs like modules, and even some one-off scripts can also participate in a pattern implementation, not just having a specific constellation of classes.
[1] Holub on Design Patterns
No! Patterns are just crutches for missing language features or "design patterns are bug reports against your programming language." GoF patterns are concepts useful in OOP, but the recurring patterns and architectures you see in other paradigms are totally different. And they don't apply to Lisp: https://www.norvig.com/design-patterns/ Most don't even apply in Go: https://alexalejandre.com/programming/software-architecture-...
> visitor is type switching, singleton global variables, command and strategy are first class functions with closures, state is building a state machine with first class functions
If you could perfectly compress your code, repeated patterns would be factored away. Macros do this. In Lisp, when you find a pattern, you write code which generates that pattern, so you don't have to.
https://mishadoff.com/blog/clojure-design-patterns/
> if patterns are discovered, arise out of independently invented idioms
Yes. That's the point behind Christopher Alexander's pattern concept - he found architectural patterns which seemed to promote good social habits, happiness etc. Gabriel's Patterns of Software presents this far better than GoF. I strongly suggest you read it: https://www.dreamsongs.com/Files/PatternsOfSoftware.pdf
Yes, Holub mentions C folks had to make do with what they had--but that those implementations, despite differences, would point to one or another common pattern (or done enough times similarly to be a pattern).
> bug reports against your programming language
Yes and no. I daresay no programming language can expect to be a universal one and still have the same ergonomics as a purposeful one or even a DSL.
At the same time, with so many amazing languages out now, new language authors may see adopters clamor for features seen in those contemporary ones.
> Macros... Lisp... Clojure
Yes, definitely have tooling for boilerplate. But that also doesn't mean the pattern could have been implemented differently as yet another idiom, or written in a way that isn't idiomatic according to one group or another.
Thank-you for the references.
We've only been speaking in terms of single languages, though. Have you combined different programs together before and thought, "Hmm, this is kind of like a pattern for..."?
That would speak to the universality of patterns.
It's metacircular all the way down. In Forth or Tcl, every command can be its own program. Deploying over many machines has similar dynamics to multithreading on one etc. The same concepts, notation etc. apply - you abstract over them and voila. Better primitive sets make things manageable, or don't.
You have reversed the point of patterns, to be a tool of thought and something to aim for. Instead, they are something you notice by a certain outcome, which guides you when you want that result again.
> Lisp ... tooling for boilerplate
That's where you've missed it. This isn't some functional propaganda; there are many paradigms, OOP and functional are just single ones. You are thinking in terms of boilerplate when these other paradigms just don't have any of it. You can even do OOP without the boilerplate - it's incidental to your tools, Common Lisp with CLOS is an OOP language. The patterns in Lisp architecture are about fundamental issues of domain modeling, how to structure teams, organizations and manage who should implement what. But you can even jump higher in scope and model that in code and "compile your company". As the code executes, at some points it will ask for user input, having an accountant do so and so action or asking a committee to assemble for something else. My company works this way.
-------
> no programming language can expect to be a universal one and still have the same ergonomics as a purposeful one or even a DSL
Yes and no. Yes, overfitting a tool to the current problem space restricts it, but DSLs can have the exact same ergonomics as any other language. Cf. Language Oriented Programming: https://beautifulracket.com/appendix/why-lop-why-racket.html
Agreed. Dr. Samek says as much too and presents an object-oriented version of C, C+, in Practical Statecharts in C/C++.
Thank-you for your replies. If you happen to see this comment, I wanted to ask about reification.
The pattern is the idea, the design reifies--"makes real"--the pattern. Something else--whether code, constellation of programs, or pigeons--implements the design.
There can be many reifications of a pattern, and there can be many implementations of a design.
Do those ideas ring true?
> compile your company
I've seen this done. Human-readable instructions coexist with SQL snippets and scripts. However, once it becomes tribal knowledge, folks may not understand it.
If manual approvals had also been processed through a pipeline instead of emails and "stop and start," it may have still been in use.
Another aspect of FP is compositionnality.
Singleton -> memoization of zero-arg fn (pro: initialization is implicit)
Memoization -> fn accepting a fn and returning a memoized version
Command & Strategy -> just a lambda then ?
Objects/classes are complex. Composing them is not simple, it means composing every single method. In fact composition is handled as a special case of class derivation, interface implementation or wrapper class implementation usually, i.e. you gotta write another class. OOP has given up on this issue from the beginning. There is no point in building a framework to create classes by composing other classes because you'll have to control how each single method compose with its counterpart. Say farewell to your neat composition expressions, the granularity is not there. Just write another class instead.Can you elaborate? Types don’t contain data in any way that I understand… Do you mean constrain?
Types are just a layer on top of your code that help enforce certain logical / mathematical properties of your code; those that your particular type system enables you to constrain.
Most actual type systems are not powerful enough to allow you to fully specify the logical objects you are actually working with, but thinking about exactly what they are and how to constrain them as well as you practically can (or should) is in my experience one of the important skills of an advanced programmer.
By the way, if you know what I mean and have found a good way to effectively teach this way of thinking, let me know, because I have often been unsuccessful. I see people left and right thinking about code as a procedure that will with trial and error ultimately be made to work, rather than as an implementation of logical / mathematical objects that can be correct or not.
I like thinking of types as a syntactical layer, just like in natural language. Without knowing the meaning of words, we can show that certain compositions of verbs, nouns, etc. can a priori never lead to an intelligible sentence. So trying to actually turn the sentence into a meaning or reading it (compiling/running) is useless.
But then again I was trained as a linguist so…
I don't know if I really agree with that. It seems to presume the default form of data is a map<any,any> and you constrain that down to a specific set of fields. (Or similar logic over void pointer spaghetti.) If you build up a record type from scratch you're doing something quite different. Adding a field where there used to be nothing, no way to store data there in an instance, is not what I would call a constraint.
The form of data in a computer is simply a sequence of binary information. It is up to you as a programmer to decide what it represents.
In some cases, you can use a type system to have that automatically enforced in part.
It's important to realize that the types you can use in practice as part of a formal type system are very rarely correct in the sense of fully describing what some data represents.
For example, if a string of bytes represents a HTTP request, then "valid HTTP request bytes" is the correct type, but the type known to the compiler might just be string of bytes.
Similarly, if you represent a graph by a hash table of int to list of int (with the ints labeling nodes), then the type of that is a map from int to list of int, where the int values in the latter must exist as keys as well - but the type system might not actually know that.
In practice, there will therefore often be functions that validate the contents of the data - these are basically manual run time type checking functions.
Let's say I'm making a new record type with a .firstname field.
And let's say I'm not declaring what data type is stored in the field. Zero description or constraint is happening there.
The .firstname field itself was unrepresentable before I added it. There is no previously existing concept of accessing a .firstname field that I am constraining.
In this scenario, my language doesn't have access to raw memory, and even once I create this record type I don't know how it's going to be stored in memory. So I'm not constraining raw pointers into organized records because there are no pointers at all. I don't have raw byte access like in your HTTP example.
In that scenario, what am I constraining? What is the unconstrained version?
The mock itself may simply be a new type defined in your test case.
You certainly would want to use an `interface` and that means you need an object. It could be an object that has no fields though and receives all data through its methods.
But it does go against the spirit of objects: You want to make use of `this` because you are in the land of nouns.
Run a copy of the server in a container, run client E2E tests against it.
> we [snip] use encapsulation." "Yes, but why?"
Simple: because when your program gets large you cannot know everything needed to modify the code. You need places where you can say "something really complex happens here and I must trust it is right" (not to be confused with it is right, only that right or wrong is irrelevant)
OOP is a great answer to the problems of large complex programs. It is not the only answer. It sometimes is the best and sometimes not. Applying it like a religion is wrong.
Why not, again?
The larger cost is maintenance. Every time you want to change/add/remove something you now need to touch not only every place that uses the thing in question, but also all the tests. That is by mocking your are tightly coupling your tests to how your code is implemented instead of the code should do. If there is more than one implementation this is okay as the code can do different things in different situations anyway, but if there is only one then you are adding coupling that isn't useful.
If you have never worked with non-trivial code that is more than 10 years old the above won't make sense. However as code ages you will discover that the original beautiful architecture ideas where always wrong for some need that happened since and so you have had to change things. Mocks make all those changes much harder.
> mocks/fakes do not count
Why not?
> In general if there is only one implementation just use it.
This is good advice for people who don't understand what interfaces are for -- which seems to be most people. If you make the mistake of believing that interfaces are for describing the commonalities between similar implementations, then demanding an IFoo for every Foo is indeed a waste. Generally if I see literally the same word twice, but one has an "I" in front of it, it's a flag that the interface might not need to exist. I think this is the advice you're giving here. But you've missed the other important point: interfaces are not "about" their implementations, they're "about" their consumers. The point of an interface is for the consumer to define exactly what it needs to do its job. That's why interfaces can be useful even if there are 0 implementations: it's still a clear definition of what some service or function requires. Assuming that service isn't private/internal, it's a hook that a consumer of your module can implement if they choose.
I'd say the frequency of the following patterns is correlated with different levels of code quality -- sometimes causally, sometimes spuriously.
1. StringWarbler : IStringWarbler (worst)
2. StringWarbler (fine)
3. StringWarbler : IWarbler (maybe worse or better, but a bit smelly)
4. StringWarbler : ITextProcessor (good)
Yes, #2 is fine if all you need to do is warble strings and there's really only one thing that means. That's common and it's fine. #3 is suspect because it looks like it was abstracted along the wrong line. It looks like there were a lot of different "ThingWarblers" and someone tried to define what was similar about them, without really focusing on the consumers of these services. Whereas with #4, it looks like someone identified a need to process text in various ways, and one way to do that is to warble them. It sounds like the interface was created to describe a need, not an implementation. When you do it this way, you start to see classes that implement two or three interfaces as well.When I see #2 everywhere, I don't think "these classes need to be implementing more interfaces!". Instead I think "the services relying on these classes are probably not doing a good job specifying exactly what they need to do their job, and are therefore brittle against changes in how those needs are fulfilled." Whether that's a problem depends on the exact situation. I feel like your advice is "prefer #2 over #3", which I generally agree with. But the better advice is "ask for what you need to do your job, in the most general form you can", which #2 (in excess) violates.
IME, people don't actually “reason better with objects”, either.
Excel is a pure functional programming language, BTW.
Unfortunately YES, because of "Java Entreprise" pushed by consultancies 15 years ago, a lot of developers insist on encapsulating everything, even when it's redundant.
Fortunately, Java is a better language today.
> a lot of developers insist on encapsulating everything, even when it's redundant.
Can you give an example? 15 years ago was 2010 and Java 8 was already released. > Fortunately, Java is a better language today.
In what ways? To me, it has barely changed since JDK 8 when it got lambdas. To be clear, the JVM is leaps and bounds better each long term supported release.Independently none of these are game changing, but together they provide much improved ergonomics and can reduce boilerplate. Now if only we had null safe types <3.
There are plenty of examples where you want to use an abstraction over various immutable record types. Services vs. records is a false dichotomy and there is power in mixing the two.
Yes, there are lots of functions that don't make sense to co-locate with their operands. Static classes are fine for those functions if both of these are true: the function requires no dependencies, and there is only one reasonable implementation of the function. In practice I find it rare that both of these are true. With a good Dependecy Injection system (and bad ones abound), requesting a service instance is just as simple as referencing a static class.
There's probably a deeper question, how to make objects 'transposable' in general case (like going from row-based to column-based representation) without duplicating code or exposing internals?
But you're right that it's commonly nice to operate on sets of things instead of individual things. If I were offsetting millions of points repeatedly, I'd look hard for a good data structure to optimize for whatever I'm trying to do.
"Exposing Internals" is not really the big issue here; the big issue is resilience against change. The time when it's appropriate to finely optimize CPU cycles is long after that code has settled into a very durable form. It's just that, for most systems, there's a lot more time spent in the volatile stage than the durable stage. Get your Big-O right and don't chatter over networks and you won't need to worry about performance most of the time. It's much rarer that you don't have to worry about change.
May I ask for one or two examples?
For a more complex example, let's consider mixing colors.
Say we have red = ColorRGB(196, 0, 0) and blue = ColorRGB(0, 0, 196) and our current task is "allow colors to be mixed". Which looks better:
purple = red.MixWith(blue) or
purple = Color.Mix(red, blue) ?
There are many different ways to mix colors, and many other things you might want to do with them too. When you start getting into things like red.MixHardLight(blue) vs.
ColorMixing.HardLight(red, blue)
The advantage of the latter becomes more clear. And it naturally extends into moving your mixing into an interface instead of a static class, so you can have "ColorMixer.Mix(red, blue)", etc. It's about focusing more on the operation you're doing than the nouns you're doing them on, which just tends to be a cleaner way to think about things. There's just a lot more variety in "different operations you can do with things" than in "types of things", at least in the kind of software development I've experienced.> there is only one reasonable implementation of the function
this. In the absence of polymorphism, a static function is just fine. I am in the phase of avoiding object notation in preference of functional notation whenever possible. I leave OO notation to cases where, for example, there is polymorphism as the functional wrapper will add little to no value.
DI in .NET is very good and you can access an object with ease with DI. Still, why use it? It's another layer between the caller and calee. Creating objects and resolving those through DI takes some CPU cycles without any major added benefits and your code becomes more complex so more things can go wrong.
Lasagna is when you have your software organized in layers. In other words instead of having a big ball of mud where A calls B calls C calls A calls C calls B you have layers (like in lasagna) so that A calls B calls C and you keep your code base so that classes/modules/types that are in the lower layer do not depend on or know of the anything above them and the dependencies only go one way.
I love lasagna. It's great (both as design and as food) !
https://stackoverflow.com/questions/2052017/ravioli-code-why... https://en.wikipedia.org/wiki/Spaghetti_code#Ravioli_code
A related principle that I don't think is talked about enough is "locality": I'd rather have all the code about one feature in one file or close together, rather than it strewn across files where it's harder to read and understand as a whole. Traditional Java was notorious for being the opposite of this. Traditional HTML+CSS+JavaScript is also very bad for this problem.
> Traditional Java was notorious for being the opposite of this.
Is modern Java better? If so, why? Also, what languages do it better than Java and why? Any as old as Java that do it better?I have this issue with: Java, Kotlin, Erlang, and Elixir, but especially Java and Kotlin.
Does object handle everything? Of course not, but having it as a capability at hand allows some neat tricks and tidy code.
I believe every problem needs a different mix of capabilities/features, and that particular mix directs my choice about which tools to use. This mindset always served me well, but who knows, maybe I'm the dumbest one in the room.
I also write my templated queries myself and talk with the server directly. No need to go "FizzBuzz Enterprise Edition" for a simple task.
In your case, if the data is not fitting into neat boxes, it's not being forced into that in the first place. I select tools/features according to the problem at hand, not try to fit what I have into the problem at hand. If that requires a new language to learn, I'm down with that too. This is why I learnt Go, for example.
Sometimes the design stretches to its limits. If that happens, I stop, refactor and continue development. I call this outgrow/expand model.
It's not the fastest way to develop software, but it consistently gives me the simplest architecture to implement the requirements, with some wiggle room for the future.
For example, the latest tool I have written has broken the design I made in the beginning, because I overgrown what I designed (i.e. added more functionality than I anticipated). Now, I'm refactoring it, and will continue adding things after refactoring.
Every tool, every iteration brings learnt lessons, which are stored in a knowledge base.
What I am saying is that Objects are not rows. And we sometimes try to force the Object model onto a data-schema that ultimately is more rich than a chain of objects.
Which is not using the most appropriate tool for the job, per your example.
An ORM is _fine_ for stuff that has a fairly standard shape, like blob posts, user accounts, things like that. Lots of relation questions against persisted data end up not being those exact shapes and patterns yet folks generally reach for the ORM.
Again that's a bedroom thought, maybe people do this with Category Theory in a haskell library, or caml modules, I'm just not aware of it.
[0] Then there are monadic types to embed a secondary type but that seems too restrictive still.
Or imagine those functions are part of the immutable record and create new instances. The aspect of (im)mutability is orthogonal to where you place your logic. In the context of domain models, if the logic is an inherent part of the type and its domain, then there are good reasons to model the logic as part of the type, and those have nothing to do with Java or the typical OOP dogma (Rust chrono: `let age = today.years_since(birthday)` - Yes, you could argue that the logic is part of the trait implementation, but the struct's data is still encapsulated)
> because in OOP land
Speaking on behalf of enterprise CRUD devs (the vast majority of programmers), the Java POJO (pure data struct) is alive and well.
You have to use encapsulation, inheritance and polymorphism. Fields and properties shouldn't have public setters. You assign a value only through a method, otherwise you make the gods angry.
You have gazillions of constructors, static, public, protected, private and internal. And you have gazillions of methods, static, public, private, protected and internal.
You inherit at least an abstract class and an interface.
You have at least some other types composed in your type.
Unless you do all this, some people might not consider them proper types.
I had my engineering manager warn me that data classes are "anemic models". Yes, but why? "We should have logic and methods to set fields in classes". "Yes, but why?" "It's OOP and we do DDD and use encapsulation." "Yes, but why? Imagine we have immutable records that hold just data and static classes as function containers, and those functions just act on the records, return some new ones and change no state. This way we can reason with ease about our goddam software, especially if we don't encapsulate state and mutate it all over the place, Uncle Bob, be damned." He shook his head in horror. He probably thinks I am a kind of heretic.