Let's consider a problem which a hypothetical do function might help us to solve:
Given an object, return the square root of the number represented by the string at path
a.b.c. Do not assume, however, that the patha.b.cwill exist.
Here's the obvious naive solution:
// naive :: Object -> Number
const naive = o => Math.sqrt(parseFloat(o.a.b.c));
This won't handle all the following inputs:
{a: {b: {c: '2.25'}}}{a: {b: {c: 'blah'}}}{a: {b: {}}}{a: {}}{}This is how I would solve this problem today:
// safe1 :: Object -> Maybe Number
const safe1 = R.pipe(
S.get('a'),
R.chain(S.get('b')),
R.chain(S.get('c')),
R.chain(S.parseFloat),
R.map(Math.sqrt)
);
With two small changes we can make this more uniform:
// safe2 :: Object -> Maybe Number
const safe2 = R.pipe(
S.Just,
R.chain(S.get('a')),
R.chain(S.get('b')),
R.chain(S.get('c')),
R.chain(S.parseFloat),
R.chain(R.compose(S.Just, Math.sqrt))
);
If we then changed the type from Object -> Maybe Number to Maybe Object -> Maybe Number we could remove the first step of the pipeline:
// safe3 :: Maybe Object -> Maybe Number
const safe3 = R.pipe(
R.chain(S.get('a')),
R.chain(S.get('b')),
R.chain(S.get('c')),
R.chain(S.parseFloat),
R.chain(R.compose(S.Just, Math.sqrt))
);
Now we have a form which can surely be abstracted! Given a list of functions we want to apply R.chain to each, then apply R.pipe to the list as positional arguments:
// $do :: Monad m => [(a -> m b), (b -> m c), ..., (y -> m z)] -> m a -> m z
const $do = fs => R.apply(R.pipe, R.map(R.chain, fs));
This can be written point-free:
// $do :: Monad m => [(a -> m b), (b -> m c), ..., (y -> m z)] -> m a -> m z
const $do = R.pipe(R.map(R.chain), R.apply(R.pipe));
Let's rewrite safe3 in terms of $do:
// safe4 :: Maybe Object -> Maybe Number
const safe4 = $do([
S.get('a'),
S.get('b'),
S.get('c'),
S.parseFloat,
R.compose(S.Just, Math.sqrt),
]);
$do works for any Monad, and should reduce demand for "composed-chain" functions such as S.gets.
What do you think? Shall we add this? If so, what should it be named? S.do is problematic since do is not a valid identifier. S.$do is pretty ugly. Does do go by other names?
wow, that is nice sugar. FWIW, I think $do is fine
wow, that is nice sugar.
I think so too! Shall I open this for discussion on the Ramda issue tracker? The function could be defined there instead. :)
your call. looks pretty interesting to me.
Can't you also do this:
// $do :: Monad m => [(a -> m b), (b -> m c), ..., (y -> m z)] -> m a -> m z
const $do = R.apply(R.pipe, R.map(R.chain));
?
It also seems like there is a way to write this with ap, but don't have time to prove that right now
The types don't line up in your definition, @buzzdecafe. You're fully applying (ahem) R.apply (which takes two arguments).
Why isn't do a valid identifier?
do is a keyword: do...while.
yep, i was reading apply like compose for some reason
Oh, duh. :goat:
The only limitation I see here is that this form of do notation only really allows you to feed through a single monadic value. You lose the ability to bind different variables to different monadic values in the same do expression like the following example:
foo :: Maybe String
foo = do
x <- Just 3
y <- Just "!"
Just (show x ++ y)
The only limitation I see here is that this form of
donotation only really allows you to feed through a single monadic value.
True. R.compose and R.pipe share this limitation, of course.
R.compose and R.pipe share this limitation, of course.
I don't really see how that relates. I think you might be able to come up with a way to bind different variables but I'm guessing it would be pretty hacky.
I think this is cool for the single pipeline case though!
I think you might be able to come up with a way to bind different variables but I'm guessing it would be pretty hacky.
indeed. I was trying to come up with some way to do that, but all i could come up with could only generously be called "hacky"
Does
dogo by other names?
The name you're looking for is Kleisli: https://en.wikipedia.org/wiki/Kleisli_category
What you've Greenspun'd is exactly Kleisli composition. :)
The name you're looking for is
Kleisli
It sounds as though Ramda needs _more_ composition functions. R.composeK and R.pipeK, anyone?
:scream:
Sorry, I didn't actually look close enough. $do isn't entirely kleisli composition. If you have:
// kleisli : Chain m => [a -> m b, b -> m c, ..., y -> m z] -> a -> m z
Then yes.
Also, note that you don't actually need the full power of Monad for this.
It sounds as though Ramda needs _more_ composition functions.
R.composeKandR.pipeK, anyone?
No, it definitely needs less (ideally one). They're all specific cases of a more general Semigroupoid idea.
They're all specific cases of a more general Semigroupoid idea.
I recall you and @scott-christopher discussing that in the gitter room. i may take a whack at unifying our many composers into one.
Which raises the thought: A good name for a javascript composition lib would be Bach.js
@joneshf, do you think such a function should return a -> m z rather than m a -> m z?
I'm having a bit of trouble seeing how something of the shape
R.pipe(
S.Just,
R.chain(f),
R.chain(g),
R.chain(h),
R.chain(i),
R.chain(j)
);
can be rewritten as a pipeline where every function in the pipeline is a partially applied R.chain.
@joneshf, do you think such a function should return
a -> m zrather thanm a -> m z?
I was just nitpicking, and looking more closely at the haskell stuff.
They're equivalent, so either way is fine, since (f >=> g) x = f x >>= g and x >>= f = (id >=> f) x. So choose either one and you can provide the other easily.
I'm having a bit of trouble seeing how something of the shape
...
can be rewritten as a pipeline where every function in the pipeline is a partially appliedR.chain.
Hmm, I think I'm having a brain fart here, or I'm not understanding the question.
They're equivalent, so [鈥 choose either one and you can provide the other easily.
That makes sense. :)
I think I'm having a brain fart here, or I'm not understanding the question.
I didn't see at the time how to return a -> m z rather than m a -> m z. I do now. The implementation is a bit more involved, since one must apply R.chain to all the arguments but the first:
// $do :: Chain m => [(a -> m b), (b -> m c), ..., (y -> m z)] -> a -> m z
const $do = R.pipe(R.converge(R.prepend,
R.head,
R.pipe(R.tail, R.map(R.chain))),
R.apply(R.pipe));
[Ramda's various composition functions and the proposed Kleisli composition functions are] all specific cases of a more general
Semigroupoididea.
I'm intrigued by the possibility of a composition function which can perform Kleisli composition as well as "regular" function composition. I've no idea how this is possible. :)
Thinking about it now, I think the only way to make it happen is to wrap the monadic function in a Kleisli type.
Also, if it wasn't clear from the signature Kleisli == ReaderT, and a -> b == ReaderT Identity a b. So that's why I feel these can be unified. But I think without a compiler implicitly passing around things and wrapping/unwrapping, it'd end up being a less than optimal api.
But I think without a compiler implicitly passing around things and wrapping/unwrapping, it'd end up being a less than optimal api.
That's often where the rubber meets the road with JS libs. There a lot of '_almost_'s and '_if only_'s floating around, usually having to do with its type system.
Thinking about it more, you'd run into the same api issues in haskell. And the ways around it are just as suboptimal, but for different reasons. I guess I just want too much.
ramda/ramda#1212 added a variadic version of the proposed $do function as R.pipeK.
@davidchambers, any interest in revisiting this now that Sanctuary is no longer dependent on Ramda?
Absolutely! Let's open a new issue: pipeK is more limited than Haskell's do notation because it doesn't provide access to the results of all previous computations. Would you mind opening an issue for pipeK, @gabejohnson?
I can do that.
I've also been looking at arrows and categories. Seems like something that would be useful to add to the spec.
Looks like there's already an issue for it https://github.com/fantasyland/fantasy-land/issues/208
See #380
Most helpful comment
I recall you and @scott-christopher discussing that in the gitter room. i may take a whack at unifying our many composers into one.
Which raises the thought: A good name for a javascript composition lib would be Bach.js