In my experience learning, teaching, and watching other people learn FP, it's far easier to learn FP with a language which is actually designed for it, e.g., Elm.
JavaScript isn't that. It carries too much legacy baggage. Too many gotchas; too many pointy bits. It's also just super noisy.
There's a reason why someone had to write The Good Parts. I don't buy the idea either that modern JavaScript is somehow devoid of the kinds of problems that existed when Crockford wrote that book. I mean, how many people still think that `const` gives you an immutable value?
n.b. I'm not sure why the Haskell snippet was included. It's confusing, and doesn't even make sense given that `main` is defined twice.
This is my experience as well. I had a co-worker that really pushed for FP in JavScript using Ramda.js. the principles were appealing, but because JavaScript doesn't have native syntax for many of the principles like pattern matching, you end up with hard to read code that's a bunch of nested arrays or chains wrapped in helper functions like "compose([fn1, fn2])(data)".
I later tried to learn F# and while the syntax was foreign, it turned out that it was MUCH easier to write FP code because the language had native operators for composition, etc. The code was also much easier to read.
I think it would be great if JavaScript got some FP syntax natively (see the pattern matching proposal). But hacking functional principals using arrays and helper functions just leads to hard to read code, especially for people not familiar with the specific FP library you chose to use.
I'm actually thinking about putting together a series of posts on the topic of "Full-stack ClojureScript for JavaScript developers". I've heard ClojureScript described (from a functional programming standpoint) as "JavaScript, but without the stupid parts", as it basically gives you the benefits of Ramda.js + Immutable.js without the headaches of making those libraries play nicely with native JavaScript. It also doesn't veer too far into the theoretical (monads, category theory, etc) so it would make for an easier transition when coming from JavaScript (you can even throw exceptions if you want).
I became a Clojure fan-boy over the course of the last year but one issue that seems to keep popping up when I discuss it with my JavaScript/Node.js co-workers is "Clojure runs on the JVM? Not interested".
Setting aside the technical validity of that opinion (JVM vs. Node.js), it made me wonder if there might be more interest in Clojure from the JavaScript world if they knew they could use ClojureScript for both the front-end and the back-end (on Node.js).
I think I'm like your co-workers, I'm really interested in Clojure, but admittably have that (probably unreasonable) JVM aversion. Do you mind sharing why I shouldn't?
The JVM is an excellent general purpose, multi-threaded runtime, with a large ecosystem of open source libraries, and its well suited to many types of "back-end" workloads, which is why it was the initial target for Clojure back in 2006/2007. Clojure's great interop with its host platform made it easy for people to get up and running while leveraging all the existing Java libraries they were used to.
The JVM is still a great choice for all those reasons, but its a little heavier than Node.js which specializes in IO bound use cases (i.e. the back-end-for-front-ends that sit between the browser and the heavyweight back-end systems). Node.js is also probably better suited for smaller services, lambdas, and other light-weight scenarios (IOT?).
I'm interested in promoting a full-stack approach to ClojureScript because the JavaScript/Node.js ecosystem is everything that the Java ecosystem was 15 years ago. It has lots of developers, lots of libraries, and a "bad" language. And by sticking with Node.js, its one less hurdle for JavaScript developers to clear when learning ClojureScript. They still get to use all the same libraries they did before (via ClojureScript's great interop), and the runtime characteristics they are used to. Plus, if they then decide to start looking at Clojure on the JVM in the future, they will have already learned the language.
Do you have a link to the pattern matching proposal? IMO if you added pattern matching and made everything (blocks, if-else, switch statements, etc) expressions then JavaScript would be pretty decent for writing in a functional style.
I have written non trivial projects with ramda and I like it a lot. You have to use your judgement with readability. Some of the advanced functions are a little hard to grok at first, but most of the library is pretty straight forward.
I think it depends. It's hard to learn more than one thing at once; if you already know JS well, learning about functional programming using it makes sense to me. It's tricky to learn both a new language paradigm and a new syntax at the same time.
Of course there are limitations to how functional you can really be in JS; that said, I like the idea of introducing some basic concepts with JS. If people like the idea, they can pursue a "functional first" language.
Yeah, this is what happened to me. FP was really easy purchase for me with JS because of its numerous flaws. But in the end, it got tiresome to have to write libs and import them around to keep the ick away, and because the lang ecosystem was doubling down on enterprise OOP conventions with TS, I decided to move away to Haskell.
When you make an absolute claim like "Learning new syntax really isn't hard." you're really saying "I find it easy therefore everyone else must too." That's poor quality thinking at best, and actively toxic to your peers at worst.
In my twenty+ years as a mentor to developers I've learned that objective statements about what is and isn't hard in programming are always going to fail when it comes to some set of devs. Some people find it trivial to move from one mental model to another. Other people find it really hard. This is true for every aspect of development - there is nothing that everyone finds easy or that everyone finds hard. If you don't take that in to consideration when you talk about programming you're always going to be failing at least one group.
I think at some point we need to stop infantilising our peers. It isn't toxic when one child says to another that speaking in Pig Latin isn't hard. The difference between `function add(a, b) { return a + b; }` and `add a b = a + b` is about as superficial as the difference between Hello World and Ellohay Orldway. In fact, as I've already alluded to, syntax swaps don't even need to span different programming languages — "modern" JavaScript uses different function expression syntax.
Describing syntax swaps as a "move from one mental model to another" is intellectually dishonest.
Indeed. Programming with Functions != Functional Programming.
FP is predicated on referential transparency. FP decides the order of operations, not the programmer. If you don’t have that, you don’t have FP: you’ve got procedural programming with closures. Which is nice, but claiming you’re doing “Functional Programming” smells of this:
An effective tool is defined as much by what it cannot do as by what it can. In FP’s case, forbidding the unpredictability of mutable-state-over-time permits the automation of higher-level mathematical reasoning, enabling both compiler and runtime to make their own optimizations as they see fit: stronger guarantees of program correctness, deferring costly operations until/if they’re needed, caching the results for fast cheap reuse, parallelizing calculations without fear of races. Powerful stuff, as long as you’re willing to cede a bit of control.
If C and its procedural ilk are all a Swiss Army hammer, declarative programming systems (functional, logic, dataflow, etc) are these:
In addition, it becomes a real pain when using generics with constraints in TS. TS doesn't support either type classes or module/namespace level generics so you end up duplicating the same constraints applied to generic parameters across several functions.
It is often easier to just use stateless classes instead and incur the runtime overhead of instantiation even if that object serves no purpose.
Are you implying that const _doesn’t_ give you an immutable value? Or are you purposefully ignoring the difference between a primitive value and a pointer to make your point?
const value can be redeclared in a different scope - but to be very precise; this is not really question of (im)mutability - it is a different value inside a scope, the original value is not changed.
Other thing is (and I believe this is what the OP is asking you) - if you say:
const a = 1;
a will always be 1 (inside the scope).
Sure if you say:
const o = { a: 1};
you can change the value of a inside the o, but the o (the pointer as the OP is saying) is not changed. I have 0 experience with Elm, but a _lot_ of popular languages have this behaviour for const/final
It's not useful to describe the behaviour of "pointers" in JavaScript, because JavaScript does not have pointers[0].
I think the article I linked to earlier was already sufficiently unambiguous. To clarify once again: If you can mutate a value, then the value is mutable. Mutable means it is not immutable. The `const` keyword in JavaScript does not give you an immutable value.
I used the terminology that the OP used to be more clear. Pointer or reference in this context is not so significant as is that 'const' is marking the reference (OP used "pointer") as a constant, and not the value that is referring to.
Point the OP was getting at:
'const' gives you an immutable reference to a mutable value because it is a modifier for the reference and not the value.
Yes, the article you linked was unambiguous, but for another question :)
As i said, a lot of langugaes (I don't know any that behaves differently) uses the const/final modifiers in this way.
In my experience with people that are just learning how to code, it is a more efficient to point out what is the 'const' modifier making a constant of, than pointing out what it does not do, because the latter sometimes sends a message that the 'const' is useless as it 'does not do anything'.
Yes precisely this. The person above you might be unaware of the fact but JavaScript (and all other languages that I’m aware of) does indeed make use of pointers albeit indirectly. When you use const and initialize it as a JS object the value in the variable is a “pointer” (call it what you will, it points to or references a memory location) and is indeed immutable. You cannot change the value of the variable. You can, however, change the value of a different variable (such as a property of the object it references) but at that point you’re not longer talking about the same variable. So in a sense “no, it’s not immutable” but in another sense, yes it actually is. It makes the most sense to talk about the value that the variable holds rather than the object it references when speaking of immutability because there also exist primitive values in JS that are immutable in exactly the same way, except they are not references (or pointers) they are just values. So instead of needing to keep the additional “gotcha, object references are immutable but the properties of that object are not frozen” you can simply understand the literal semantics. Of course that is just my personal opinion
Do you think your 200-word explanation is easier for a beginner to understand than "this value is immutable — it will never change"?
My initial argument was that JavaScript is a terrible choice for learning FP, precisely because it has so many caveats and is notoriously poorly understood, even by people who use it every day. Your lengthy explanation proves my point.
I find this argument a bit disingenuous to be honest, I took the liberty of spelling things out in my reply because I assumed you wouldn’t otherwise be able to see the point. An explanation to a beginner, which doesn’t require memorizing any additional gotchas, would be something like “variables hold values. Strings, numbers, booleans, and references are values. Variables declared with const contain values that are immutable. An object is not a value.”
IMO Redux is a terrible way to start learning functional programming. There is too much boilerplate code and other code (ie. reducer, selectors, side effects, react components) that will confuse beginners and make the goal of learning functional programming harder.
It's like telling someone who wants to learn to drive, here's the road laws book, car manual, car service manual, offroad rally driving guide and engine tuning manual.
Redux was my first introduction to FP, and I loved it. You simply have to use Redux Toolkit that takes care of the ugly parts of Redux (namely: the boilerplate code), and it becomes a breeze to build things with Redux.
While I use Redux, and I like the simplicity of the single application state and the reducer function. I don't see it as a good example of FP or even JavaScript.
The problem with Redux is that it takes many patterns that make sense in languages like Haskell or Elm but are not idiomatic in JS. For example, action types and the usual reducer switch statement is easier to write and extend in Haskell.
And is not idiomatic JavaScript either. It always baffles me that an action dispatch is a way of doing a method dispatch (and yes I know that unlike a method dispatch an action can be persisted, and broadcasted). But if you compare the boilerplate and concepts introduced by a simple action dispatch (action creator, action type, payload, thunk, middleware) with a simple method call, and you look at the benefits (late binding with the state, persistence)... I wonder why solutions like Zustand didn't come up before.
(note, I wrote state management code similar to Zustand for my personal projects, but one of the reasons to continue using Redux is that getting the Context updates right is hard... a problem solved by React-Redux with addition of nice tooling and collaboration from core React devs. As soon React adds fine-grained context state updates the reasons to continue using Redux for me are low).
Side note: I just published a brand-new "Redux Essentials" core docs tutorial. It teaches beginners "how to use Redux, the right way", using our latest recommended tools and practices, including Redux Toolkit for writing your Redux code, the React-Redux hooks API for interacting with Redux in your components, and use of single-file "slices" for a given feature's Redux logic. I'd encourage folks to check it out:
My next step will be to rewrite the existing "bottom-up" tutorial sequence to simplify explanations, remove outdated references, improve the explanation flow, and add more running examples.
I'd recommend caution when adopting redux-toolkit in large & rapidly-evolving projects.
I recently (2 months back) moved a medium sized project (~240 branch reducers, ~600 actions) away from redux-toolkit.
My primary complaint is that the recommended setup doesn't work well with changing requirements where we often have to move away from branch-local state handling to something that needs access to wider state.
We started out with a number of slices and our reducer logic was local to these slices and used only the state within that slice. However as our application evolved, for processing a number of these actions (which were previously slice-local) we needed access to state from other branches. So now we had a couple of options:
A. Dispatch thunks instead of actions: This gets ugly real fast because now your action handling logic is split across thunks & reducers, and is hard to follow. This also needs refactoring across every dispatch site.
B. Use something like redux sagas to intercept actions: We found this to be "too" flexible and felt that we were better-off without the entire machinery of spawning sagas on the fly. We wanted it to be easy to reason about what happens when an action is dispatched looking at the code without having to debug what all sagas could be running at that particular point of time.
C. Move the action handling higher up: requires extensively refactoring the reducers.
The solution we settled on was redux-loop [1]: A port of elm's effect system to redux. This was neat because we could easily convert the reducer to return loops instead of states, and thereby easily get access to full state and dispatch while retaining the ability to follow through the complete action handling flow from a single starting point that didn't change.
TypeScript support in redux-toolkit is also kind'a bolted on and users are recommended different approaches when they care about type-safety. It proved to be a pain to communicate junior devs multiple times that you should use leave the reducers as empty object and instead use "extraReducers" with builder API.
We found using immer[2] (for managing immutable state) and unionize[3] (for handling discriminated union of action types) directly to be a much better solution than redux-toolkit's abstractions.
RTK doesn't change anything about how you write reducers and actions that need to interact across slices. Our recommendations have always been the same: reorganize slice reducers so they handle more state, put more data in actions, or coordinate via side effects [0].
The only thing that changes with RTK is that we now recommend using the single-file "slice/ducks" pattern for a given feature's Redux logic, and RTK's `createSlice` API makes it easier to write code that's organized that way. If you do have cross-slice dependencies, where slice A and B both want to respond to each other's actions, that _could_ potentially lead to cyclic dependency issues. Our RTK Usage Guide page specifically addresses that question [1], and resolving it is generally a matter of defining the relevant actions in a separate file again. But, having logic in a single file by default drastically simplifies things in most cases. This issue isn't unique to RTK - it exists any time you're trying to have different slices depend on each other, regardless of how the logic in those files are implemented.
Also strongly disagree that RTK's TS support is "bolted on". RTK is written in TS, and we've spent hundreds of hours trying to ensure our APIs work well with TS [2]. We test against multiple TS versions, design our APIs to minimize the amount of types you have to declare, and try to offer the best type safety possible with our preferred API structure.
The only time you would ever define `createSlice.extraReducers` as an empty object is if you are _only_ using that slice as a data cache and not adding any additional client-side logic that would manipulate that cached data. In that case, you'd probably be better suited to use `createReducer` directly.
Having said that, we are currently working on a PR to add the ability to declare async thunks directly inside of `createSlice` [3], leveraging our new `createAsyncThunk` API [4] so that their action types are automatically generated to match the slice name and the action creator name you specify. That will eliminate the need to call `createAsyncThunk` separately and pass its actions to `createSlice.extraReducers`.
Finally, RTK has been built around Immer since day 1, and it's used in `createReducer` and `createSlice` to allow you to write simpler "mutating" immutable update logic.
If you have any additional specific concerns, please ping me on Twitter or in the Reactiflux Discord. I'm always happy to answer questions and offer suggestions, and I would really be interested in seeing some details on the app you're working on to see if there's any ideas for improving RTK's APIs for your kind of use case.
I've been doing haskell for a few months now. I don't think I have ever seen a expression like this. It looks like State. Is this even valid syntactically? I'm Probably bikeshedding.
It's still there as far as I can see, it's in an image[0]. I believe their intention is to show a kind of pseudo-desugaring of what the IO monad actually does internally.
JavaScript isn't that. It carries too much legacy baggage. Too many gotchas; too many pointy bits. It's also just super noisy.
There's a reason why someone had to write The Good Parts. I don't buy the idea either that modern JavaScript is somehow devoid of the kinds of problems that existed when Crockford wrote that book. I mean, how many people still think that `const` gives you an immutable value?
n.b. I'm not sure why the Haskell snippet was included. It's confusing, and doesn't even make sense given that `main` is defined twice.