State of the book.
First of all awesome book πThank you for creating it.
I noticed that the last time a chapter was added was 2017. I was wondering: What is the state of the other chapters? Are chapters 13+ in the works? Could we add information about them into the readme?
Thanks! I have 1 chapter left to write which is a "real world" pure app. Are there other's you'd like to see?
@DrBoolean Thank you for answering. I'm looking forward to you finishing the book π₯ Also watched your course on egghead, great stuff.
I don't know what you are covering in the next chapters, and how the pure app looks, but here are some things that I struggle with when trying to use functional programming (in JavaScript):
-
Usefully composing and piping functions. I often find myself producing shit loads of spaghetti code. (See here for an example.) I guess practice makes perfect, but any aid on how to know how to compose smartly would be awesome.
-
Using FP in React. I love React and use it every day in my free time and for my job. Hooks have made it much more functional. Some examples how to structure your components in an FP way would be awesome. Do you split in display / container components? Do you
connectRedux (HOC) or do you useuseSelector? When do you introduceEither,Box,Maybe,Tasketc.? -
Side effects. I still find it hard how and when to handle side effects when using FP.
Obvious cases are where you first fetch and then handle data:
const transform = R.map(myFuncDoesStuff);
function Example() {
const [state, setState] = useState(...);
async function fetchData() {
const res = await fetch(...);
setState(transform(res));
}
}
But what if you have to alternate between fetching, posting and calculating? How do you use IO, Maybe and co in React?
I'm no @DrBoolean and I'm still a beginner at using this stuff in JavaScript, but if it's any use, I recently found myself reaching for Maybe and Either in a React app like so:
Suppose I have an app that makes a fetch request when it mounts. I might have initial state that looks like:
{
isFetching: true,
error: '',
data: null
}
The fetch call is in flight, and when I get a response, I'll set isFetching to false and either populate the error field or the data field depending on what I get back. (Not sure if this looks familiar, but I came across a lot of resources that represent "fetching" state in this way).
A problem with modeling our app state like so is that there is a tacit understanding that there are certain states of the app that I don't want to be possible, but that I haven't explicitly made impossible to represent, such as if I'm done fetching but I still don't have data or errors:
{
isFetching: false,
error: '',
data: null
}
That doesn't make sense (or I don't want it to make sense, and I haven't accounted for that in any of my views, but I'm not doing anything to rule that out as a possibility). Neither does the case in which I'm still fetching but somehow have data and/or an error:
{
isFetching: true,
error: 'Failed to fetch: network error',
data: [
{ name: 'Joey' },
{ name: 'Johnny' },
{ name: 'Dee Dee' },
{ name: 'Tommy' }
]
}
So what I could do instead is represent my state as Maybe (Either Error Data) and that encodes the three possible states that my app could be in:
function App() {
// data :: Maybe (Either Error Data)
const [data, setData] = useState(Nothing)
useEffect(() => {
fetch(API)
.catch(error => setData(Just.of(Left.of(error))))
.then(data => setData(Just.of(Right.of(data))))
// or
// .catch(compose(setData, Just.of, Left.of))
// .then(compose(setData, Just.of, Right.of))
}, [])
return maybe(Loading,
either(onError, onSuccess))(data)
// where
// Loading :: JSX
// onError :: Error -> JSX
// onSuccess :: Data -> JSX
}
That's a pretty simple example, and there may be better ways of accomplishing the same, but it helped me wrap my head around how to use some of this stuff to represent the possible views in an app, for example.
@ptrfrncsmrph Thank you very much! I think it's a shame that there is so little content about FP in the real world and how you would use it with React. Your example is much appreciated π
@ptrfrncsmrph @janhesters If you are interested in real-world frontend FP I suggest you have a look at elm.
I've been through Elm, PureScript, Haskell and Scala and I can't recommend Elm more. This language is simple and elegant with a 100% pure interface and, seemingly no runtime-errors (even with JavaScript interop).
@KtorZ Thank you for your suggestion π€ I've already looked into Scheme by reading "Structure and Interpretation of Computer Programs" and I can confirm that looking at pure languages helps a lot.
I planned to look at "Learn you a Haskell" next to learn more about FP. I'm curious, why would you suggest Elm over Haskell?
PS: Keep in mind my goal is not to switch languages, but to get better at FP in JavaScript (preferably in conjunction with React).
I'm curious, why would you suggest Elm over Haskell?
For actually many reasons. Elm is a nice "gateway drug" into Haskell actually because of its syntax and ideas similarity. However, a few things that make Elm nice to learn for improving your FP skillz, especially coming from JavaScript and before even learning Haskell:
-
Elm syntax is extremely small, and can be learnt within a very short time
-
Elm primitive types are quite similar / dual to JavaScript's types (because ultimately, it's compiled to JS). Haskell can be more overwhelming at first with
Int,Natural,Word8,Char,Word32,Word64,Float,Double,Fractional,String,Text,ByteString,Lazy.TextLazy.ByteString... and so forth -
Elm utilizes the ideas of FP, mostly everything that you've seen in this book, though, without much of the complexity / mathematical overhead that could come along. There's no mention of
MonadorFunctorper se in Elm, though, you'll recognize them throughList.map,Maybe.andThen,Task.andThenfor instance. -
There's no ad-hoc polymorphism in Elm (no type classes) which makes the language slightly more "boilerplate-ish" to some degrees, but with the great trade off of simplicity. Sounds like a limitation initially, but turns out to make code a lot easier in the long-run
-
Elm is super easy to setup, especially coming from JavaScript.
npm install -g elmand you're done. The compiler is dead fast, and because it produces a JS bundle, it's really easy to "translate" an existing development pipeline (webpack or whatever you use) to Elm. -
The compiler is really friendly and helpful... like.. you have no idea. That's not something you can remotely imagine to have seen anywhere else. Haskell compiler can be really confusing at first (and even after a while) especially when you start stacking extensions (needed for most Haskell programs...). I can't count the number of time I've seen GHC whining that "The Impossible Has Happened!" anymore, plus other confusing messages. In Elm, the compiler is like a true programming assistant / teacher.
-
Elm is 100% pure which is, not the case with Haskell (and most FP languages actually...). In Haskell, you can still write partial functions, or "bottoms" (values that always type check but will throw a runtime exception if evaluated!), and write very imperative code by using primitives such as
MVarorIORef(basically mutable variables). So, because you have access to great power, it requires more care and more discipline in order to not screw up. Elm is quite different in the sense that, you don't have access to this power. Again, it looks like a limitation at first, but it turns out it's one of the greatest thing the language has in its design. You can't escape the type-system, you can't fool the compiler and you have to be pure. So, in the end, it forces you to think differently and solve problem in a truly FP fashion, whereas in Haskell you could (and will) cheat in some cases.
All-in-all, Elm is just very very suitable for learning and long-term projects because it drags the maintenance cost down. It forces you to adopt a purely functional mindset and, the tooling is simple and friendly :)
@KtorZ Thank you very much, that was a very convincing pitch! I'm gonna look at Elm now first π
Just to give another concise example for my problem 1.
- Usefully composing and piping functions. I often find myself producing shit loads of spaghetti code...
Here is another situation where I struggle to write code imperatively rather than declaratively.
TLDR: I have problems writing code declaratively when it involves logic (like if statements) and / or reusing values to not compute the same thing over and over (like an index). Here is the (pretty imperative) function:
import * as R from 'ramda';
const getFunction = ({ state, payload } = { state: [] }) => {
const index = R.findIndex(
R.allPass([
R.propEq('key', R.prop('key', payload)),
R.propEq('id', R.prop('id', payload)),
]),
state
);
if (index > -1) {
if (state[index].original === payload.value) {
return R.remove(index, 1);
} else {
return R.update(index, { ...payload, original: state[index].original });
}
} else {
return R.append(payload);
}
};
Verbose context:
I have a reducer that should save contact changes, meaning it consists of an array of changes. Changes consist of a key, a value, the original value and an id for the contact.
The reducer should:
- If there is no change for the given
idandkey, add the change. - If there is a change for the
idand thekey, and i.) If the change is different from theoriginal, replace the change, but copy the original. ii.) If the change sets the contact back to original, remove the change.
The getFunction I wrote above does exactly that, but it's pretty imperative. It gets called in the reducer like this:
changeContact: (state, payload) => getFunction({ state, payload })(state),
How would you write this declaratively? Should you even write it declaratively?
If you want it even more verbose, here is the reducer's full code and the tests its supposed to pass:
import autodux from 'autodux';
export const {
reducer: changedContacts,
actions: { changeContact, clearChangedContacts },
selectors: { getChangedContacts },
} = autodux({
slice: 'changedContacts',
initial: [],
actions: {
changeContact: (state, payload) => getFunction({ state, payload })(state),
clearChangedContacts: () => [],
},
});
import { describe } from 'riteway';
describe('changed contacts reducer', async assert => {
assert({
given: 'no arguments',
should: 'return the valid initial state',
actual: changedContacts(),
expected: [],
});
assert({
given: 'initial state and a change contacts action',
should: 'add the contact change',
actual: changedContacts(
undefined,
changeContact({
id: '1',
key: 'firstName',
value: 'foo',
original: 'bar',
})
),
expected: [{ id: '1', key: 'firstName', value: 'foo', original: 'bar' }],
});
assert({
given:
'changed contacts and a change contact action for a different contact',
should: 'add the new contact change',
actual: changedContacts(
[{ id: '1', key: 'firstName', value: 'foo', original: 'bar' }],
changeContact({
id: '2',
key: 'firstName',
value: 'foo',
original: 'bar',
})
),
expected: [
{ id: '1', key: 'firstName', value: 'foo', original: 'bar' },
{ id: '2', key: 'firstName', value: 'foo', original: 'bar' },
],
});
assert({
given: 'changed contacts and a change contact action for a different field',
should: 'add the new contact change',
actual: changedContacts(
[{ id: '1', key: 'firstName', value: 'foo', original: 'bar' }],
changeContact({
id: '1',
key: 'lastName',
value: 'foo',
original: 'bar',
})
),
expected: [
{ id: '1', key: 'firstName', value: 'foo', original: 'bar' },
{ id: '1', key: 'lastName', value: 'foo', original: 'bar' },
],
});
assert({
given: 'changed contacts and a change contact action for the same change',
should: 'replace the change, but keep the original',
actual: changedContacts(
[{ id: '1', key: 'firstName', value: 'foo', original: 'bar' }],
changeContact({
id: '1',
key: 'firstName',
value: 'baz',
original: 'foo',
})
),
expected: [{ id: '1', key: 'firstName', value: 'baz', original: 'bar' }],
});
assert({
given: 'changed contacts and a change contact action to the original',
should: 'remove the change',
actual: changedContacts(
[
{ id: '1', key: 'firstName', value: 'foo', original: 'bar' },
{ id: '2', key: 'firstName', value: 'foo', original: 'bar' },
],
changeContact({
id: '1',
key: 'firstName',
value: 'bar',
original: 'foo',
})
),
expected: [{ id: '2', key: 'firstName', value: 'foo', original: 'bar' }],
});
assert({
given: 'changed contacts and a clear changed contacts action',
should: 'return the initial state',
actual: changedContacts(
[
{ id: '1', key: 'firstName', value: 'foo', original: 'bar' },
{ id: '2', key: 'firstName', value: 'foo', original: 'bar' },
],
clearChangedContacts()
),
expected: [],
});
assert({
given: 'changed contacts and a get changed contacts selector',
should: 'return the current changed contacts',
actual: getChangedContacts({
changedContacts: [
{ id: '1', key: 'firstName', value: 'foo', original: 'bar' },
],
}),
expected: [{ id: '1', key: 'firstName', value: 'foo', original: 'bar' }],
});
});
Thank you at anyone for any help if anyone reads this, btw π
@janhesters Well, if you wanted a much uglier version than yours, here it is: https://codesandbox.io/s/get-function-lv8w8
I'm also learning to do this. Yeah, mine uses more R stuff, but it feels much more convoluted. The logic should be the same though.