gvergnaud / ts-pattern

🎨 The exhaustive Pattern Matching library for TypeScript, with smart type inference.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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