fpdart
fpdart copied to clipboard
Do Notation
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 TaskEither
s, 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!
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 🔜
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 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 thaterror
hasvalue
when usingtryCatch
-
getOrElse
gets the value fromRight
, andthrow
whenLeft
(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?
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 I put this together for Option
in a new branch.
I added 3 functions to Option
:
-
DoInit
: InitialiseOption
directly from the Do notation -
Do
: Start a Do notation from the currentOption
-
DoThen
: Start a Do notation ignoring the currentOption
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?
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 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 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 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.