Support point-free style
kabo opened this issue · comments
Is your feature request related to a problem? Please describe.
When doing functional programming it's common to things like pipes or promise chains. This ends up looking something like
functionReturningPromise()
.then((res) => match(res).with(...).with(...).exhaustive())
or
import * as R from 'ramda'
const myFunc = R.pipe(
R.pluck('thing'),
R.find(R.propEq(true, 'a')),
(item) => match(item).with(...).with(...).exhaustive()
)
See how one needs to create a function to get the input only to pass it into match
?
Describe the solution you'd like
It would be neat if there was a match
that took the input last, so one could write like this
import { match } from 'ts-pattern/fp'
functionReturningPromise()
.then(match.with(...).with(...).exhaustive())
or
import { matchFp } from 'ts-pattern'
import * as R from 'ramda'
const myFunc = R.pipe(
R.pluck('thing'),
R.find(R.propEq(true, 'a')),
matchFp().with(...).with(...).exhaustive()
)
Or it could be done with different syntax, perhaps something like this
import { match } from 'ts-pattern/fp'
functionReturningPromise()
.then(match({
with: [
[ pred, fn ],
[ pred2, fn2 ],
],
exhaustive: true,
}))
This would make it much more intuitive to use in an FP / point-free context.
Describe alternatives you've considered
One could perhaps get away with just writing some sort of wrapper around match
.
Hi!
Thanks for your proposal, I agree it would be neat for ts-pattern to support a point free version of match
. The only problem is that I think it would be challenging to have good type inference if the input type isn't provided explicitly.
TS-Pattern uses the input type to infer:
- the structure of a pattern, and provide auto-complete suggestions for object keys and literal values.
- If the expression is exhaustive or not.
Without the input type, there is no way to do either of these.
In your option 1 and 2, since the returned expression would have a call signature (it would be a function taking the input value), I think we could make TS forward the input type as a parameter to the resulting function, but we can't back-propagate it to previous .with
calls, which means the only thing we could check is exhaustiveness.
In your option 3 (the one where with
and exhaustive
are passed as a data structure), I think we could provide the same level of inference as we have today. I'm not fond of having 2 different syntaxes for the same thing though. I think it would be pretty confusing so that's my least favorite option anyway.
Another option we could consider is this one:
import { match, with, exhaustive } from 'ts-pattern'
functionReturningPromise()
.then(
match(
with(...),
with(...),
exhaustive()
)
)
Since the top level expression returns a function, we should be able to get the input type in this case. But with
is a reserved keyword in JS, so we can't have a variable with that name.
Maybe something like:
import { match, input } from 'ts-pattern'
functionReturningPromise()
.then(
match(
input
.with(...)
.with(...)
.exhaustive()
)
)
All in all, this looks like a lot of work and a significant API extension only for a marginal DX improvement. I'm not sure this would be worth it, but I'd be happy to be convinced otherwise.
I started tinkering with the wrapper idea.
Code
This does run, but doesn't seem to do much in terms of compile time checks/helping out.import { match } from 'ts-pattern'
interface ExhaustiveArg {
readonly type: 'exhaustive'
}
interface RunArg {
readonly type: 'run'
}
interface WithArg {
readonly type: 'with'
readonly pred: any
readonly fn: any
}
interface WhenArg {
readonly type: 'when'
readonly pred: any
readonly fn: any
}
interface OtherwiseArg {
readonly type: 'otherwise'
readonly fn: any
}
type MatchArg = WithArg | WhenArg
type ReturnArg = ExhaustiveArg | OtherwiseArg | RunArg
type MatchArgs = [ ...MatchArg[], ReturnArg ]
const withFn = (pred, fn): WithArg => ({
type: 'with',
pred,
fn,
})
const when = (pred, fn): WhenArg => ({
type: 'when',
pred,
fn,
})
const otherwise = (fn): OtherwiseArg => ({
type: 'otherwise',
fn,
})
const exhaustive = (): ExhaustiveArg => ({ type: 'exhaustive' })
const run = (): RunArg => ({ type: 'run' })
const helper = (...args: MatchArgs) =>
<T>(input: T) =>
args.reduce((matcher, arg) =>
arg.type === 'with' ? matcher.with(arg.pred, arg.fn)
: arg.type === 'when' ? matcher.when(arg.pred, arg.fn)
: arg.type === 'otherwise' ? matcher.otherwise(arg.fn)
: arg.type === 'run' ? matcher.run()
// @ts-expect-error not callable?
: arg.type === 'exhaustive' ? matcher.exhaustive()
: matcher
, match(input))
console.log([{a: true}, {a: false}].map(helper(
withFn({a: true}, () => 'a is true'),
withFn({a: false}, () => 'a is false'),
exhaustive()
)))
Not sure that's the right track though...
Perhaps take inspiration from another project like Effect? https://effect.website/docs/style/match
Looks like one needs to provide the type explicitly and upfront. e.g. something like this?
import { match, input } from 'ts-pattern'
functionReturningPromise()
.then(
match(
input<MyInputType>()
.with(...)
.with(...)
.exhaustive()
)
)
I think needing to provide the input type is an acceptable tradeoff for not having to do (input) => match(input)...
. Would be nicer if it could infer it, but still an improvement.
Thoughts?
the input solution
"data-first" pipe API can solve the issue maybe ?
there is an explanation here https://github.com/remeda/remeda