Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Breaking refactor and re-implementation of effects. #6

Merged
merged 24 commits into from
Sep 26, 2018

Conversation

mrozbarry
Copy link
Collaborator

@mrozbarry mrozbarry commented Sep 21, 2018

Summary

This branch represents an internal re-write of the piece that runs the app. Since the mainline now has decent test coverage of how the app is run, I am confident to make internal changes, along with some external.

Changes

Effects as messages

A big change that will affect the code other developers write is how effects are handled. Previously, effects looked like this:

const { Effect } = ferp.types;

// No effect:
Effect.none();

// Immediate message:
Effect.immediate({ type: 'YOUR MESSAGE' });

// Deferred effect:
const { dispatch, effect } = Effect.defer();
setTimeout(dispatch, 1000, { type: 'YOUR MESSAGE' });

// Multiple effects:
const { dispatch, effect } = Effect.defer();
setTimeout(() => dispatch({ type: 'YOUR MESSAGE' }), 1000);
Effect.map([
  Effect.none(),
  Effect.immediate({ type: 'YOUR MESSAGE' }),
  effect,
  Effect.map([
    // ...etc.
  ]),
]);

I had two main thoughts: first, I don't really like using classes in functional code, and second, this looks much more complicated than it has to. After some thought, and a few renames, I came to this conclusion:

const { effects } = ferp;

// No effect:
effects.none();

// Immediate message:
{ type: 'YOUR MESSAGE' };

// Deferred effect:
effects.defer(new Promise((resolve) => {
  setTimeout(resolve, 1000, { type: 'YOUR MESSAGE' });
}));

// Multiple effects:
effects.batch([
  effects.none(),
  { type: 'YOUR MESSAGE' },
  effects.defer(yourPromiseHere),
  effects.batch([
    // ...etc.
  ]),
]);

none, defer, and batch are the core effect types, which can be wrapped and extended to do more interesting behaviours like delays, fetching data, etc. If your effect does not match a core type, it is treated as an immediate message.

Inspecting an effect, it may look something like:

const effect = { type: Symbol('batch'), effects: [] };

Previously, effects were messages wrapped in promises, but outside of a deferred message, this was a shoe-horn, and not really the right data type for effects. I'm also using symbols for types so they don't collide with developer-provided message types.

init isn't a function

Originally, I liked init being a function since update, and subscribe where also functions, so it made the app code look more uniform, but it's become apparent to me that this is not at all necessary.

Previously, giving an initial state and effect looked like this:

const { app } = ferp;

app({
  init: () => [1, ferp.types.Effect.none()],
  // ...
})

And now it looks like this:

const { app } = ferp;

app({
  init: [1, ferp.effects.none()],
  // ...
});

No more middleware

This was a harder decision that I'm still really thinking through. Originally, I wanted to rename and restructure middleware to listeners, methods that had no return value or control over update, but would be called after each update with the message that triggered the update, and the next state.

Originally, I like the rename, but it became apparent to me that this was starting to build an unnecessary feature. I think the answer for middleware is encouraging developers to use high-order functions to wrap their main update method. I've demonstrated this in examples/cli/updateLogger.js.

Result as data

Once I removed class Effect, it was clear that the result type would need some thought put into it to align with the rest of ferp. I especially didn't like how exposing the Result type meant writing code to enforce the result states.

Since middleware and the logger were removed, serialize wasn't necessary to keep, and I didn't like having an exposed constructor, so moving to plain objects seemed like the next best move, especially considering the success I had with the effects messages.

That said, the outward api is almost identical. Here is the original class based results:

const { Result } = ferp.types;

Result.none();
Result.pending();
Result.done();
Result.error();

const myResult = Result.none();
const resolvedData = myResult.get(
  () => 'none',
  () => 'pending',
  (value) => ['done', value],
  (err) => ['error', err],
);
const defaultedData = myResult.getWithDefault(
  (value) => ['done', value],
  () => 'some default here',
);

And with the new result module:

const { result } = ferp;

result.nothing();
result.pending();
result.just();
result.error();

const resultedData = result.get(
  () => 'nothing',
  () => 'pending',
  (value) => ['just', value],
  (err) => ['error', err],
)(result.nothing());

const defaultedData = result.getWithDefault(
  (value) => ['just', value],
  () => 'some default here',
)(result.nothing());

One thing is I wanted to align better with Elm's maybe union, which is something like type Maybe = Nothing | Just a.

I'm still on the fence with this even being in this library, since internals don't use it, and I only have a single example. I'd love to get feedback on whether or not this is something that ferp should provide.


Note: This is a bit of a living document while I continue to review and refactor/clean up code.

@mrozbarry mrozbarry added the wip Work in progress, feedback welcome label Sep 21, 2018
@mrozbarry mrozbarry force-pushed the experiment/app-runner branch 2 times, most recently from 2258c4f to 5ae1435 Compare September 21, 2018 13:48
@mrozbarry mrozbarry changed the base branch from master to next September 26, 2018 16:52
@mrozbarry mrozbarry changed the title WIP Experimental app running mechanism Breaking refactor and re-implementation of effects. Sep 26, 2018
@mrozbarry mrozbarry merged commit df23e1f into next Sep 26, 2018
@mrozbarry mrozbarry deleted the experiment/app-runner branch September 26, 2018 16:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
wip Work in progress, feedback welcome
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant