Some somewhat common constructions using monadic composition are more complex than they have to be. Applicative solves that by being less powerful and lacking context sensitivity (commonly):
do
a <- digitP
b <- digitP
c <- digitP
d <- digit
return $ IP a b c d
where digitP = digit >>= \x -> skip '.' >>= \_ -> return x
-- vs
pure IP <$> digitP <*> digitP <*> digitP <*> digitP <*> digit
where digitP = digit <* (skip '.')
The default implementation of Applicative for a Monad also works for Chomp:
instance Applicative X where
pure = return
d <*> e = do
b <- d
a <- e
return (b a)
(*>) = (>>)
x <* y = x >>= \a -> y >> return a
But the issue here is that to be able to write things like
IP must be a function which is partially applied and is invoked like IP(192)(168)(0)(23) which is both very strange to use in Rust (which does not support partial application natively) and very slow since it forces boxed closures to be used to store the intermediate state.
Implementation
[x] pure
Same implementation as Input::ret, use Input::ret instead of pure.
[ ] <*>
Problematic due to evaluation order and no partial application, see [Implementation] section.
[x] *>
Does not need any implementation, just use then.
[x] <*
Name skip, simple and no special considerations: self.bind(|i, t| rhs(i).map(|_| t))
Input::ret is kept as is to provide a way to lift values into the Applicative. then is also left as is as a method on ParseResult. skip is added to impl ParseResult since it is very useful even in monadic chaining.
apply is either implemented on a trait in a separate module or directly on the ParseResult.
trait ApplyArgs<F> {
type Output;
type Error;
fn apply(self, Input, F) -> ParseResult<Self::Output, Self::Error>;
}
impl<A, AT, AE, T, F> ApplyArgs<F> for A
where F: FnOnce(AT) -> T,
A: FnOnce(Input) -> ParseResult<AT, AE> {
type Output = T;
type Error = AE;
fn apply(self, i: Input, f: F) -> ParseResult<Self::Output, Self::Error> {
self(i).map(f)
}
}
// Impls for tuples, eg. (A, B) where F: FnOnce(A, B) -> Out, A: FnOnce(Input) -> ParseResult<A, AE>, ...
impl ParseResult<T, E> {
pub fn apply<A>(self, rhs: A) -> ParseResult<A::Output, A::Error>
where A: ApplyArgs<T>,
A::Error: From<E> {
self.bind(|i, f| rhs.apply(i, f))
}
}
Pros
Allows for arbitrary number of parameters
Compact syntax in usage
Does not treat functions and values differently
Applicative laws still apply if translated to tuples for multiple arguments
Cons
Fails to properly infer for closures in tuples like (|i| i.ret("foobar"), ...) (It cannot determine that i is Input) since it is using several traits, struct ParseResult -> trait ApplyArgs -> trait FnOnce -> concrete closure
Not very ergonomic at times since types have to be declared for all parameters.
Not possible to stack any values conditionally, the specific expression has to be known at compile time
Not as composable as the default since the tuples do not lend themselves well to partial application (which is kind of necessary to be able to express Applicative laws in a simple manner).
Note that it does not play well with existing values in the Monad/Applicative since they are of type T which is not a tuple while the Applicative functions (ap and apply) only work on tuples in the applicative. IE. Input + ParseResult<T, E> is always a Monad, but only an Applicative if T is a tuple of some kind.
Problem
Some somewhat common constructions using monadic composition are more complex than they have to be.
Applicative
solves that by being less powerful and lacking context sensitivity (commonly):The default implementation of
Applicative
for aMonad
also works for Chomp:But the issue here is that to be able to write things like
IP
must be a function which is partially applied and is invoked likeIP(192)(168)(0)(23)
which is both very strange to use in Rust (which does not support partial application natively) and very slow since it forces boxed closures to be used to store the intermediate state.Implementation
[x]
pure
Same implementation as
Input::ret
, useInput::ret
instead ofpure
.[ ]
<*>
Problematic due to evaluation order and no partial application, see [Implementation] section.
[x]
*>
Does not need any implementation, just use
then
.[x]
<*
Name
skip
, simple and no special considerations:self.bind(|i, t| rhs(i).map(|_| t))
Input::ret
is kept as is to provide a way to lift values into the Applicative.then
is also left as is as a method onParseResult
.skip
is added toimpl ParseResult
since it is very useful even in monadic chaining.apply
is either implemented on a trait in a separate module or directly on theParseResult
.Variants
There are multiple ways of implementing
apply
Following Haskell's default Applicative
Pros
Cons
Monad laws
Tuple as an argument
Pros
Cons
(|i| i.ret("foobar"), ...)
(It cannot determine thati
isInput
) since it is using several traits,struct ParseResult -> trait ApplyArgs -> trait FnOnce -> concrete closure
Applicative laws
Stacking tuples
The idea here is to stack values into a tuple and then invoke a function with the given values:
Pros
Cons
T
since it will conflict with all the impls for tuples).Applicative laws
This version reverses the order of the
apply
operator arguments while still keeping the left to right ordering of the arguments supplied toap
:Note that it does not play well with existing values in the Monad/Applicative since they are of type
T
which is not a tuple while the Applicative functions (ap
andapply
) only work on tuples in the applicative. IE.Input
+ParseResult<T, E>
is always a Monad, but only an Applicative ifT
is a tuple of some kind.