fpdart icon indicating copy to clipboard operation
fpdart copied to clipboard

Do Notation

Open prazedotid opened this issue 2 years ago • 1 comments

Hello!

Thank you for your awesome package, really helped me implement most of the concepts I learned from fp-ts in Flutter.

But I do think it's missing something, and that is the Do Notation. I currently have a use case where I need to preserve values across multiple TaskEithers, and I think the Do Notation approach can solve this.

Is there going to be a Do Notation implementation soon in fpdart? Or do you have an alternative approach to my case?

Thank you!

prazedotid avatar Oct 14 '21 19:10 prazedotid

Hi @prazedotid

Yes, I agree that the Do Notation would be awesome. It is certainly in the package scope. It requires some work to be implemented properly, but it will come to fpdart as well if possible 🔜

SandroMaglione avatar Oct 24 '21 14:10 SandroMaglione

Been playing with a do notation implementation using async / await functions. Reasonably simple, but it does have some trade-offs:

typedef TaskEither<L, R> = Future<Either<L, R>> Function();

typedef _DoAdapter<E> = Future<A> Function<A>(TaskEither<E, A>);

_DoAdapter<L> _doAdapter<L>() => <A>(task) => task().then(either.fold(
      (l) => throw either.UnwrapException(l),
      (a) => a,
    ));

typedef DoFunction<L, A> = Future<A> Function(_DoAdapter<L> $);

// ignore: non_constant_identifier_names
TaskEither<L, A> Do<L, A>(DoFunction<L, A> f) {
  final adapter = _doAdapter<L>();
  return () => f(adapter).then(
        (a) => either.right<L, A>(a),
        onError: (e) => either.left<L, A>(e.value),
      );
}

Usage:

final TaskEither<String, int> myTask = Do(($) async {
  await $(left("fail")); // execution will stop here

  final count = await $(right(123));

  return count;
});

Because it is async / await, it doesn't prevent someone from using a normal Future generating function, but if you are aware of the limitations it can help clean up complex code :)

tim-smart avatar Nov 27 '22 23:11 tim-smart

@tim-smart this looks really interesting 💡

I've played around with it, and this is the result (using TaskEither as class from fpdart):

class _EitherThrow<L> {
  final L value;
  const _EitherThrow(this.value);
}

typedef _DoAdapter<E> = Future<A> Function<A>(TaskEither<E, A>);

_DoAdapter<L> _doAdapter<L>() => <A>(task) => task.run().then(
      (either) => either.getOrElse((l) => throw _EitherThrow(l)),
    );

typedef DoFunction<L, A> = Future<A> Function(_DoAdapter<L> $);

TaskEither<L, A> Do<L, A>(DoFunction<L, A> f) => TaskEither.tryCatch(
      () => f(_doAdapter<L>()),
      (error, _) => (error as _EitherThrow<L>).value,
    );
  • _EitherThrow makes sure that error has value when using tryCatch
  • getOrElse gets the value from Right, and throw when Left (catched later)

Which allows the following:

final myTask = Do<String, double>(($) async {
  final value = await $(TaskEither.right(10));

  print("I am doing this with $value");
  await $<int>(TaskEither.left("fail"));
  print("...but not doing this");

  final count = await $(TaskEither.of(2.5));

  return count;
});

I will work more on this, which looks promising. I want to understand the implications, the risks, and the advantages. Did you think already about a list of downsides or risks? Do you have a longer example that shows how this helps clean up code?

SandroMaglione avatar Nov 28 '22 05:11 SandroMaglione

The biggest downside is forgetting to use await when running a task only for its side effects (ignoring the result). It is easy to spot when actually using the return value.

Smaller trade offs include:

  • You can await normal Futures that could throw unexpected errors. This risk isn't very high, as using do notation is quite intentional.
  • It makes some things harder, like specifying fallback logic that would normally use alt.

It is actually probably more useful for monads like Option / Either. You can implement it in a similar fashion:

typedef _DoAdapter = A Function<A>(Option<A>);

A _doAdapter<A>(Option<A> option) => option._fold(
      () => throw None(),
      (value) => value,
    );

typedef DoFunction<A> = A Function(_DoAdapter $);

// ignore: non_constant_identifier_names
Option<A> Do<A>(A Function(_DoAdapter $) f) {
  try {
    return Some(f(_doAdapter));
  } on None catch (_) {
    return None();
  }
}

tim-smart avatar Nov 28 '22 07:11 tim-smart

@tim-smart I put this together for Option in a new branch.

I added 3 functions to Option:

  • DoInit: Initialise Option directly from the Do notation
  • Do: Start a Do notation from the current Option
  • DoThen: Start a Do notation ignoring the current Option

I tested how this looks in an example and indeed it looks neat:

/// No do notation
String goShopping() => goToShoppingCenter()
    .alt(goToLocalMarket)
    .flatMap(
      (market) => market.buyBanana().flatMap(
            (banana) => market.buyApple().flatMap(
                  (apple) => market.buyPear().flatMap(
                        (pear) => Option.of('Shopping: $banana, $apple, $pear'),
                      ),
                ),
          ),
    )
    .getOrElse(
      () => 'I did not find 🍌 or 🍎 or 🍐, so I did not buy anything 🤷‍♂️',
    );

/// With do notation
String goShoppingDo() => Option.DoInit<String>(
      ($) {
        final market = $(goToShoppingCenter().alt(goToLocalMarket));
        final banana = $(market.buyBanana());
        final apple = $(market.buyApple());
        final pear = $(market.buyPear());
        return 'Shopping: $banana, $apple, $pear';
      },
    ).getOrElse(
      () => 'I did not find 🍌 or 🍎 or 🍐, so I did not buy anything 🤷‍♂️',
    );

/// or
String goShoppingDoContinue() =>
    goToShoppingCenter().alt(goToLocalMarket).Do<String>(
      (market, $) {
        final banana = $(market.buyBanana());
        final apple = $(market.buyApple());
        final pear = $(market.buyPear());
        return 'Shopping: $banana, $apple, $pear';
      },
    ).getOrElse(
      () => 'I did not find 🍌 or 🍎 or 🍐, so I did not buy anything 🤷‍♂️',
    );

I am planning to do the same for TaskEither and see how it looks, as well as testing some of the issues you mentioned.

Did you think about some downsides also for this Do notation with the Option type?

SandroMaglione avatar Nov 29 '22 05:11 SandroMaglione

I would prefer to see only one way of initializing do notation, then composing it with other operators:

Option.of(1).flatMap((i) => Option.Do(($) {
      final sum = $(Option.of(i + 1));
      return sum;
    }));

Option.of(1).call(Option.Do(($) {
  final a = $(Option.of(2));
  return a;
}));

Option.Do(($) {
  final a = $(Option.of(2));
  return a;
});

tim-smart avatar Nov 30 '22 21:11 tim-smart

@tim-smart Agree. I left one factory constructor Do as follows:

/// Init
String goShoppingDo() => Option.Do(
      ($) {
        final market = $(goToShoppingCenter().alt(goToLocalMarket));
        final amount = $(market.buyAmount());

        final banana = $(market.buyBanana());
        final apple = $(market.buyApple());
        final pear = $(market.buyPear());

        return 'Shopping: $banana, $apple, $pear';
      },
    ).getOrElse(
      () => 'I did not find 🍌 or 🍎 or 🍐, so I did not buy anything 🤷‍♂️',
    );

/// FlatMap
String goShoppingDoFlatMap() => goToShoppingCenter()
    .alt(goToLocalMarket)
    .flatMap(
      (market) => Option.Do(($) {
        final banana = $(market.buyBanana());
        final apple = $(market.buyApple());
        final pear = $(market.buyPear());
        return 'Shopping: $banana, $apple, $pear';
      }),
    )
    .getOrElse(
      () => 'I did not find 🍌 or 🍎 or 🍐, so I did not buy anything 🤷‍♂️',
    );

/// call (andThen)
final doThen = Option.of(10)(Option.Do(($) => $(Option.of("Some"))));

I am going to work on Either and then TaskEither and see how this works.

SandroMaglione avatar Dec 06 '22 05:12 SandroMaglione

@SandroMaglione Did you need any help with this?

I'm using fpdart in a experimental package (https://github.com/tim-smart/nucleus/tree/main/packages/elemental), and Do notation for Option and Either would be really nice :)

tim-smart avatar Feb 19 '23 22:02 tim-smart

@tim-smart The Do notation is work in progress in this branch. My plan would be to release a beta version to test how it works and improve on it. Either and Option are already implemented in the branch. Aiming to publish in the next few days.

SandroMaglione avatar Feb 23 '23 16:02 SandroMaglione