Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support point-free style #209

Open
kabo opened this issue Jan 4, 2024 · 3 comments
Open

Support point-free style #209

kabo opened this issue Jan 4, 2024 · 3 comments
Labels
enhancement New feature or request

Comments

@kabo
Copy link

kabo commented Jan 4, 2024

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.

@kabo kabo added the enhancement New feature or request label Jan 4, 2024
@gvergnaud
Copy link
Owner

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.

@kabo
Copy link
Author

kabo commented Jan 8, 2024

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?

@romainPrignon
Copy link

the input solution
"data-first" pipe API can solve the issue maybe ?
there is an explanation here https://github.com/remeda/remeda

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

3 participants