gcanti / fp-ts

Functional programming in TypeScript
https://gcanti.github.io/fp-ts/
MIT License
10.75k stars 503 forks source link

Runtime Pattern Matching Example #1225

Open chrischen opened 4 years ago

chrischen commented 4 years ago

📖 Documentation

While you show an example of pattern matching here, it would be nice to have an example of how to do pattern matching with custom types. https://gcanti.github.io/fp-ts/guides/purescript.html

For example, should we just add a _tag property to all run time interface types? How should we do it for opaque scalar types, or Newtypes?

Should it be used with Branded types and simply set a runtime value on the branded type definition?

These are just some questions I have as a new user of your excellent libraries.

gcanti commented 4 years ago

While you show an example of pattern matching here, it would be nice to have an example of how to do pattern matching with custom types

Not sure what you mean, Option is a custom type.

export interface None {
  readonly _tag: 'None'
}

export interface Some<A> {
  readonly _tag: 'Some'
  readonly value: A
}

export type Option<A> = None | Some<A>

const maybe = <A, B>(whenNone: () => B, whenSome: (a: A) => B) => (fa: Option<A>): B => {
  switch (fa._tag) {
    case 'None':
      return whenNone()
    case 'Some':
      return whenSome(fa.value)
  }
}

For example, should we just add a _tag property to all run time interface types?

Yes, that's how you define sum types in TypeScript (aka Discriminated Unions). Note that _tag is just my convention, you can choose another name.

How should we do it for opaque scalar types, or Newtypes? Should it be used with Branded types and simply set a runtime value on the branded type definition?

Not sure what you mean

chrischen commented 4 years ago

How should we do it for opaque scalar types, or Newtypes? Should it be used with Branded types and simply set a runtime value on the branded type definition?

Not sure what you mean

For example branding a type makes it an intersection with { SomeTypeBranded: unique symbol }. If I then also want the type to have a runtime value such as for pattern matching, do I also still define a { _tag: 'TypeName' } property or should I just add a runtime value to the branded type like so:

interface Target {
  _tag: 'Target';
  size: number;
}
const BigTarget = t.brand(
  Target,
  (T): T is t.Branded<Target, { readonly _tag: 'BigTarget' }> =>
    Target.is(T),
  'BigTarget'
);
gcanti commented 4 years ago

If a type is not a sum type (like string, number or Int) you don't need to provide a tag field

export interface IntBrand {
  readonly Int: unique symbol
}

export type Int = number & IntBrand

export interface Person {
  name: string
  age: Int
}

You add a tag when you define a sum type

export type User = { type: 'Anonymous' } | { type: 'LoggedIn'; person: Person }

export const match = <R>(onAnonymous: () => R, onLoggedIn: (person: Person) => R) => (user: User): R => {
  switch (user.type) {
    case 'Anonymous':
      return onAnonymous()
    case 'LoggedIn':
      return onLoggedIn(user.person)
  }
}
chrischen commented 4 years ago

Is it correct to say that the branded type must be used on a non-sum type and it must be a sub-type of the input type?

gcanti commented 4 years ago

Is it correct to say that the branded type must be used on a non-sum type

@chrischen I'm not sure how these things are related to fp-ts: discriminated unions are a TypeScript feature and branded types are a hack to make up for the lack of opaque types in TypeScript (there's no branded types in fp-ts).

Anyway if you are talking about this kind of implementation of branded types:

export interface IntBrand {
  readonly Int: unique symbol
}

export type Int = number & IntBrand

then you can use Int everywhere you would use any other type, that's not different from using string or number.

Here I'm using Int in a sum type (the sum type, or "discriminated union", is User)

export type User = { type: 'Anonymous' } | { type: 'LoggedIn'; name: string, age: Int }

and it must be a sub-type of the input type?

again, if you are talking about this kind of implementation of branded types, then Int is trivially a subtype of number since is defined as

number & IntBrand
chrischen commented 4 years ago

Sorry, I meant to post this in io-ts. I was specifically referring to the brand() constructor in io-ts, and if it's meant to be used to make interface types opaque.