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

@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

@SandroMaglione SandroMaglione Apr 6, 2024

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
@lishaduck
Copy link

Hello! Is there anything I could do to help make some progress on this? I'd really like to switch some code to FPDart, but I don't want to if it'll soon need to be rewritten. In addition, I really like Effect-TS and would love to see a Dart equivalent.
I'm certainly happy to help with tests and docs, and would be willing to work through porting implementations.

@SandroMaglione
Copy link
Owner Author

Hello! Is there anything I could do to help make some progress on this? I'd really like to switch some code to FPDart, but I don't want to if it'll soon need to be rewritten. In addition, I really like Effect-TS and would love to see a Dart equivalent. I'm certainly happy to help with tests and docs, and would be willing to work through porting implementations.

The initial implementation is there. The main issue now is with composition. The dart language doesn’t support union types and type inference is limited. This means that you are required to specific manually all types, which is not ideal from a developer UX perspective. So much so that it doesn’t yet justify using fpdart's Effect over usual OOP dart patterns.

This may be solved by static metaprogramming, so I am actually waiting for some progress on the language.

@lishaduck
Copy link

lishaduck commented Jul 25, 2024

The initial implementation is there. The main issue now is with composition. … This may be solved by static metaprogramming, so I am actually waiting for some progress on the language.

Ok then. Thank you for the explanation, it's very helpful for understanding the current progress.
Would you expect fpdart 2 to be stable enough to use then, just with the expectation that it'll get more concise in the future, or do you anticipate macros to entirely subsume the current API?

@SandroMaglione
Copy link
Owner Author

The initial implementation is there. The main issue now is with composition. … This may be solved by static metaprogramming, so I am actually waiting for some progress on the language.

Ok then. Thank you for the explanation, it's very helpful for understanding the current progress. Would you expect fpdart 2 to be stable enough to use then, just with the expectation that it'll get more concise in the future, or do you anticipate macros to entirely subsume the current API?

I don't expect the Effect encoding to change. Nonetheless, static metaprogramming has the potential to disrupt many common dart patterns. I would advise against using fpdart v2 in production yet.

That being said, the API as it is now works and hopefully won't need to change too much.

@lishaduck
Copy link

lishaduck commented Jul 31, 2024

Static metaprogramming has the potential to disrupt many common dart patterns. I would advise against using fpdart v2 in production yet.

It's primarily for a personal project, so there's not really a "production".

I don't expect the Effect encoding to change ... That being said, the API as it is now works and hopefully won't need to change too much.

Ok, thanks! I guess I'll give it a go and report back with any feedback. Thanks again for fpdart!

@lishaduck
Copy link

lishaduck commented Aug 10, 2024

Finally got some time to work on migrating some code to fpdart.

Feedback:

  • I miss the default tryPromise overload.
  • I haven't used Cause much, but I'm wanting it now. I found cause right after I wrote my own impl 😀. Could it get its own file? (moved comment)
  • Is a schema package planned? Dart doesn't really have a "Zod".
  • Dart's lack of row polymorphism for records is annoying. I thought it'd be a good way to type context, but it intentionally doesn't work.

P.S.: I'll update this comment if I have anything else to add instead of notifying everyone.

Copy link

@lishaduck lishaduck left a comment

Choose a reason for hiding this comment

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

I left some comments, feel free to disregard.


typedef Exit<L, R> = Either<Cause<L>, R>;

sealed class Cause<L> implements Error {

Choose a reason for hiding this comment

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

Is there a reason to use Error over Exception? Errors shouldn't be caught, but Failures should.

@@ -0,0 +1,12 @@
part of "effect.dart";

Choose a reason for hiding this comment

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

Is there a reason to use parts? Circular dependencies?
Just curious.

@@ -0,0 +1,61 @@
import 'ordering.dart';

class Order<T> {

Choose a reason for hiding this comment

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

Yay! I love Order from Effect, looks similar.

Foldable2<_EitherHKT, L, R>,
Alt2<_EitherHKT, L, R>,
Extend2<_EitherHKT, L, R> {
sealed class Either<L, R> extends IEffect<Never, L, R> {

This comment was marked as resolved.

This comment was marked as resolved.

final class _OptionThrow {
const _OptionThrow();
}
part of "effect.dart";
Copy link

@lishaduck lishaduck Aug 11, 2024

Choose a reason for hiding this comment

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

Would a micro module be possible, like Effect's, but even lighter?
In my head, you could keep IEffect as base, but keep Option, Either, etc from directly interacting with Effect, only IEffect. That would make migrating from v1 and integrating it into existing projects easier, by not requiring users to use the Effect system (it's obviously recommended to use the Effect portion), and comes with the side-effect of a (slightly) smaller learning curve, as you can use it in chunks.

I thought there were some APIs here that called Effect. Doesn't look like anything really, so I think it should be documented that you can still use Option, Either, etc, without needing to adopt the effect system.


typedef Exit<L, R> = Either<Cause<L>, R>;

sealed class Cause<L> implements Error {

Choose a reason for hiding this comment

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

Oh, also, could this get its own module? I get that it's an internal detail, but Exit feels different than Cause. Maybe rename it rather than splitting it given the size of Exit?

);

/// {@category folding}
Effect<E, Never, C> match<C>({
Copy link

@lishaduck lishaduck Aug 11, 2024

Choose a reason for hiding this comment

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

This has a semi-non-trivial definition, so I'm hesitant to suggest this, but maybe it'd be possible to use native pattern matching? i.e.

final val = switch ($.syncEither(effect)) {
  Left() => ...,
  Right() => ...,
}

rather than

final val = effect.match(
  () => ...,
  () => ...,
)

Yes, it's more verbose, but the native pattern matching feels much more "right" to me. The function feels like it's working around an issue from JS that isn't in Dart.


Been writing some more code with fpdart, and I definitely think this could be simpler. There're plenty of functions that could just be pattern-matching, which reduces complexity/learning curve, and is more flexible.

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.

8 participants