Ramda has Kleisli composition via composeK and pipeK. We probably only need one of those.
It is worth considering instead a specification for Categories as suggested in https://github.com/fantasyland/fantasy-land/issues/208 and implementing a Kleisli type that exposes its own compose.
This would mean pipe would be sufficient if it were implemented in terms of compose.
This would mean
pipewould be sufficient if it were implemented in terms ofcompose.
Wouldn't we still need to put something into a Kleisli at the beginning and take it out at the end?
I'm thinking about some like Kleisli Arrows with a compose:
compose :: Monad m => (a -> m b) -> (b -> m c) -> (a -> m c)
I haven't thought out the particulars, but calling the result is going to return a monad.
Maybe
Kleisli :: Monad m => forall a b. m -> (a -> b) -> (a -> m b)
?
Another option is to just make Kleisli a wrapper like Endo.
const k = Kleisli(a => [a]); // The Kleisli identity?
const l = Kleisli(a => [a, a]);
compose(l, k)(1); // [1, 1]
Since pipeK would exist only for convenience, we should compare usage examples. Let's use this as the starting point:
S.pipe([S.chain(f), S.chain(g), S.chain(h)])
With pipeK:
S.pipeK([f, g, h])
With Kleisli:
S.pipe([S.Kleisli(f), S.Kleisli(g), S.Kleisli(h)])
S.pipe(S.map(S.chain, [f, g, h]))
const pipeK = S.compose(S.pipe, S.map(S.chain))
I'm happy defining it myself for my sporadic use-cases. :man_shrugging:
@davidchambers, I was thinking the same thing yesterday. I think we should still add it though.
Aldwin's implementation looks good.
Aldwin's definition is very elegant, but it relies on partial application. Since we don't use S functions internally, this implementation is our best option:
function pipeK(fs, x) {
return Z.reduce(function(x, f) { return Z.chain(f, x); }, x, fs);
}
Gabe, would you like to submit a pull request for this?
Is this released?
I'm getting:
TypeError: _sanctuary2.default.pipeK is not a function
└── [email protected]
@hbarcelos it's merged into master, but didn't make it in time for the last release. How did you find it?
I hope to release v0.15.0 this weekend, by the way. :)
@gabejohnson I saw this issue, saw the merged label and tried to use this function 😅.
@davidchambers OK, I'll wait until then :)
@gabejohnson @davidchambers Any interest in revisiting this and doing it in terms of Category (i.e. just have pipe work on categories, then have a category for kleisli arrows).
That sounds good to me, @masaeedu, provided the performance is acceptable.
How is this different from my proposal above?
It's probably not, but it looks like the implementation ended up being a specialized pipeK for one reason or another. A pipeC function that accepts the typerep of a category would result in pipeC(Function) being pipe and pipeC(Kleisli) being pipeK.
It would look something like this (but with the implementations fetched from the env by typerep):
const Fn = (() => {
const id = x => x
const compose = f => g => a => f(g(a))
return { id, compose }
})()
const Kleisli = M => {
// :: a -> m a
const id = M.of
// :: (b -> m c) -> (a -> m b) -> (a -> m c)
const compose = bmc => amb => a => M.chain(bmc)(amb(a))
return { id, compose }
}
const pipeC = C => arr => arr.reduce((p, c) => C.compose(c)(p), C.id)
// Monad for demonstrating kleisli composition
const Arr = (() => {
const of = x => [x]
const chain = f => x => x.reduce((p, c) => [...p, ...f(c)], [])
const range = n =>
Array(n)
.fill(undefined)
.map((_, i) => i + 1)
return { of, chain, range }
})()
const pipe = pipeC(Fn)
const pipeK = M => pipeC(Kleisli(M))
const tests = [
pipe([x => x + 1, x => x * 2])(5),
pipeK(Arr)([Arr.range, x => (x % 2 ? [x, '👏'] : [x, '👏 👏'])])(4)
]
console.log(tests)
There's many interesting things that form categories (among them lenses), and pipeC would serve as a convenience function for composing more than two items in any category.
Most helpful comment
I'm happy defining it myself for my sporadic use-cases. :man_shrugging: