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

Do Notation #26

Closed
prazedotid opened this issue Oct 14, 2021 · 9 comments · Fixed by #97
Closed

Do Notation #26

prazedotid opened this issue Oct 14, 2021 · 9 comments · Fixed by #97
Assignees
Labels
enhancement New feature or request

Comments

@prazedotid
Copy link

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!

@SandroMaglione
Copy link
Owner

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 🔜

@tim-smart
Copy link
Contributor

tim-smart commented Nov 27, 2022

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 :)

@SandroMaglione
Copy link
Owner

@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?

@tim-smart
Copy link
Contributor

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();
  }
}

@SandroMaglione
Copy link
Owner

@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?

@tim-smart
Copy link
Contributor

tim-smart commented Nov 30, 2022

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;
});

@SandroMaglione
Copy link
Owner

@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.

@tim-smart
Copy link
Contributor

@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 :)

@SandroMaglione
Copy link
Owner

@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 SandroMaglione linked a pull request Mar 6, 2023 that will close this issue
7 tasks
@SandroMaglione SandroMaglione mentioned this issue Mar 6, 2023
7 tasks
@SandroMaglione SandroMaglione self-assigned this Apr 28, 2023
@SandroMaglione SandroMaglione unpinned this issue May 12, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
Status: Done
Development

Successfully merging a pull request may close this issue.

3 participants