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

fpdart v2.0.0 #147

Open
wants to merge 91 commits into
base: main
Choose a base branch
from
Open

fpdart v2.0.0 #147

wants to merge 91 commits into from

Conversation

SandroMaglione
Copy link
Owner

This PR is an open discussion on what's the best strategy to release fpdart v2 to minimize friction for users of the library

πŸ‘‡ Please comment below with any idea, comment, critique or opinion πŸ‘‡


Problems with v1

Too many classes (IO, IOOption, IOEither, Task, TaskOption, TaskEither, Reader, State, ReaderTaskEither):

  • Similar implementation with different generic parameters = A lot of duplicated code (both core and tests)
/// [IO] πŸ‘‡
abstract final class _IOHKT {}

final class IO<A> extends HKT<_IOHKT, A>
    with Functor<_IOHKT, A>, Applicative<_IOHKT, A>, Monad<_IOHKT, A> {
  final A Function() _run;
}

/// [Task] πŸ‘‡
abstract final class _TaskHKT {}

final class Task<A> extends HKT<_TaskHKT, A>
    with Functor<_TaskHKT, A>, Applicative<_TaskHKT, A>, Monad<_TaskHKT, A> {
  final Future<A> Function() _run; /// πŸ‘ˆ Difference: [Future] here
}
  • Code duplication = Hard to maintain, more lines of code, more code to read (and understand) for contributors
  • Requires conversion between classes (from*, to*, e.g. toTask, toTaskEither)
  • Requires having a different Do constructor for each class, making the do notation harder to use
  • Hard to understand for newcomers, hard to reason with and explain for veterans (and verbose)
  • More complex code, less contributors

Too much jargon: methods and classes are using terms from pure functional programming (math), less clear and hard to understand (e.g. pure, Reader, chainFirst, traverse, Endo).

Typeclasses do not work well with dart and they cause a lot of overhead to maintain and understand. In fact, they are not necessary to implement the core of fpdart (they can be removed πŸ’πŸΌβ€β™‚οΈ).

Too many "utility functions" that may be considered outside of the scope of fpdart (e.g. predicate_extension.dart).

fpdart v2 solution: Effect

A single Effect class that contains the API of all other classes in v1 (similar to ReaderTaskEither).

All Effect-classes derive from the same interface IEffect:

abstract interface class IEffect<E, L, R> {
  const IEffect();
  Effect<E, L, R> get asEffect;
}

Benefits

  • A lot less code: easier to maintain, contribute, test, understand (a single effect.dart)
  • No need of conversion methods (a lot less code)
  • A single Do notation (implemented as a factory constructor factory Effect.gen): the do notation also includes Option and Either (since both extend IEffect)
  • No more jargon: easy to understand method names instead of fp jargon (e.g. succeed instead of pure)
  • Removed all typeclasses and unnecessary utility methods
  • Easier to explain and understand (focus on learning a single Effect and how it works)
  • Smaller API that allows all the same functionalities as before
  • More resources to focus on better documentation, tests, and examples

Important: Effect comes from the effect library (typescript), which itself was inspired from ZIO.

The Effect class and methods in fpdart are based on effect from typescript (similar API and methods names).

Huge thanks also to @tim-smart for his initial zio.dart implementation.

Downsides

  • ⚠️ Huge breaking change ⚠️
  • Nearly all tests need to be rewritten
  • Documentation and examples to redo completely

@SandroMaglione
Copy link
Owner Author

New release: fpdart v2.0.0-dev.2:

  • Complete Option and Either API
  • Execute Effect using provide (with Null as dependency)
  • Fixed implementation of running Effect and catching Cause
  • Added interruption (Cause.Interrupted)
    • Deferred
    • Context
    • New methods (raceAll, race, delay, sleep, timeout)

@airychkov
Copy link

Error stack not so good. Only show main function call, not show when happen

@airychkov
Copy link

I don't know it's right or not
two Fail just for test
if (!sourcePath.isEmpty) {
Effect.fail(SourceTypeFailure(error: 'empty source'));
}

final sourceType = $.sync(sourcePathType().mapEnv((final env) => sourcePath));

if (!sourcePath.isEmpty) {
Effect.fail(SourceTypeFailure(error: 'empty source2'));
}

console log:

flutter: empty source on null
flutter: empty source2 on null

@SandroMaglione
Copy link
Owner Author

New release: fpdart v2.0.0-dev.3:

  • Added Scope
  • const constructor for None

packages/fpdart/lib/src/effect.dart Show resolved Hide resolved
packages/fpdart/lib/src/effect.dart Outdated Show resolved Hide resolved
packages/fpdart/lib/src/effect.dart Outdated Show resolved Hide resolved
return deferred.wait<E>().__unsafeRun(context).then(
(exit) => signal
.failCause<E, L>(const Interrupted())
.__unsafeRun(context.withoutSignal)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like there is a bug, when the parent signal is interrupted it isn't propagated to the child signal.

packages/fpdart/lib/src/effect.dart Outdated Show resolved Hide resolved
Effect<Null, L, R> provide(Context<E> context) {
final env = context.env;
final effect = env is ScopeMixin && !env.scopeClosable
? alwaysIgnore(env.closeScope())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm this could be an issue, as if there was a defect when try to close the scope, it will be swallowed / unhandled.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added some code to handle defects when closing the scope, anything you think is missing?

Effect<E, L, R> _provideEnvCloseScope(E env) =>
env is ScopeMixin && !env.scopeClosable
? Effect<E, L, R>.from(
(context) => _unsafeRun(context).then(
(exit) => switch (exit) {
Left(value: final value) => Left(value),
Right(value: final value) =>
env.closeScope<E, L>()._unsafeRun(context).then(
(exit) => switch (exit) {
Left(value: final value) => Left(value),
Right() => Right(value),
},
),
},
),
)
: this;

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You need to close the scope regardless of success or failure.

packages/fpdart/lib/src/effect.dart Outdated Show resolved Hide resolved
Effect<Scope<E>, L, R> acquireRelease(
Effect<Null, Never, Unit> Function(R r) release,
) =>
withScope.tapEnv(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically you should make the resource acquisition uninterruptible here, would require some thought about how to make interruption toggle-able / regional.

@ryanhanks-bestow
Copy link

ryanhanks-bestow commented Apr 3, 2024

Hey Sandro.

Looking good, I've been playing around.

Given

typedef ProduceEffect<State, ProductionError, Event>
    = Effect<State, ProductionError, Iterable<Event>>;
typedef HandleEffect<State, HandlerError, Event>
    = Effect<(State, Event), HandlerError, State>;
typedef ApplyEffect<State,Error,Event> = Effect<State, Error, (Iterable<Event>, State)>

, how would you do a fold across the iterable result of a ProduceEffect using a HandleEffect to result in an Effect of type ApplyEffect?

Feel free to suggest changes in the shapes of those definitions if you have a better starting approach.

Thanks

@ryanhanks-bestow
Copy link

ryanhanks-bestow commented Apr 3, 2024

Hey Sandro.

Looking good, I've been playing around.

Given

typedef ProduceEffect<State, ProductionError, Event>
    = Effect<State, ProductionError, Iterable<Event>>;
typedef HandleEffect<State, HandlerError, Event>
    = Effect<(State, Event), HandlerError, State>;
typedef ApplyEffect<State,Error,Event> = Effect<State, Error, (Iterable<Event>, State)>

, how would you do a fold across the iterable result of a ProduceEffect using a HandleEffect to result in an Effect of type ApplyEffect?

Feel free to suggest changes in the shapes of those definitions if you have a better starting approach.

Thanks

Also, curious if you have any general suggestions in naming type alias of an effect. Those names aren't the names I'm using verbatim, but I have been applying a naming convention where I prefix an alias with a verb matching the general action within the Effect that maps an Env value to the result and/or error it could produce. So for example, in the actual code I'm working on, I've implemented typedefs as such:

typedef DispatchEffect<State, DispatchError, Event>
    = Effect<State, DispatchError, Iterable<Event>>;

typedef StateEventHandlerEffect<State, HandlerError, Event>
    = Effect<(State,Event), HandlerError, State>;

Again, not 100% sure I'm shaping things here, maybe with this you'll be able to let me know if I'm applying the general use correctly.

Thanks

@ryanhanks-bestow
Copy link

@SandroMaglione what editor do you use? In intelliJ, when I'm creating an Effect using Effect.gen, I lose type-awareness inside the call to sync (even if I explicitly supply the type parameter A).

Wondering if you might consider this alternative definition for EffectGen:

class EffectGen<E, L> {
  EffectGen({
    required FutureOr<A> Function<A>(IEffect<E, L, A>) async,
    required A Function<A>(IEffect<E, L, A>) sync,
  })
      : this.sync = sync,
        this.async = async;

  FutureOr<A> async<A>(IEffect<E, L, A>effect) => this.async(effect);

  A sync<A>(IEffect<E, L, A>effect) => this.sync(effect);

}

Comment on lines 202 to 207
static Effect<E, L, void> sleep<E, L>(Duration duration) => Effect.from(
(_) => Future.delayed(
duration,
() => const Right(null),
),
);
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is possible to use Timer+Completer.

@tim-smart How would interruption work for sleep? How can sleep detect an interrupt and block timer?

Suggested change
static Effect<E, L, void> sleep<E, L>(Duration duration) => Effect.from(
(_) => Future.delayed(
duration,
() => const Right(null),
),
);
static Effect<E, L, void> sleep<E, L>(Duration duration) => Effect.from(
(_) {
final completer = Completer<Exit<L, void>>();
Timer(duration, () {
completer.complete(const Right(null));
});
return completer.future;
},
);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First you will want to add a Effect.async/asyncInterrupt for callback apis, then use Effect.asyncInterrupt with the Timer api. Then hook up timer.cancel.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tim-smart I added async, but I am having some issues understanding the mental model for Deferred and interruption.

Could you give me an example of how to manually interrupt an Effect with code? Does this required Deferred or is it about Context? Are fibers needed?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sleep should be:

  static Effect<E, L, void> sleep<E, L>(Duration duration) =>
      Effect.asyncInterrupt(
        (resume) {
          final timer = Timer(duration, () {
            resume.succeed(null);
          });
          return Effect.succeedLazy(() {
            timer.cancel();
          });
        },
      );

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As for interrupting an Effect, here is some pseudo code:

final cancel = Effect.never.runCallback();

// some time later...

cancel();

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://effect-ts.github.io/effect/effect/Effect.ts.html#runcallback

In dart maybe something like:

void Function() runCallback({ void Function(Exit<E, A> exit)? onExit })

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is runCallback the only way to manually trigger an interruption?

How would you test interruption with sleep (or also asyncInterrupt)?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In Effect (typescript) you can pass an abort signal to Effect.runPromise, use the cancel callback from Effect.runCallback or call interrupt on fiber returned from Effect.runFork.

dart doesn't have a built in "cancellation" primitive, so you could use a Future potentially.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think is it possible to use Deferred (or Context) in fpdart to give access to interruption?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not too sure what you mean, might just have to try it out!

@SandroMaglione
Copy link
Owner Author

@ryanhanks-bestow could you provide a screenshot of IntelliJ that shows the type not inferred? Do you know of any limitation of IntelliJ regarding Records?

@SandroMaglione SandroMaglione mentioned this pull request May 21, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants