import { Head, Image } from 'mdx-deck'
export { default as theme } from './theme'
import Link from './components/Link' import Split from './components/Split'
<title>Pattern matching in TS</title> gillchristian { ' @ ' } HousingAnywhereDoes someone know what it is?
Let's define it ...
switch("hola") {
| "hola" => "HOLA"
| "chau" => "bye!"
| _ => "..."
}; /* "HOLA" */
switch("other") {
| "hola" => "HOLA"
| "chau" => "bye!"
| _ => "..."
}; /* "..." */
export default Split
Many languages have it.
In reason it's called `switch`.
import scala.util.Random
val x: Int = Random.nextInt(10)
x match {
case 0 => "zero"
case 1 => "one"
case 2 => "two"
case _ => "many"
}
In Scala it's called `match`
sealed abstract class Furniture
case class Couch() extends Furniture
case class Chair() extends Furniture
def findPlaceToSit(piece: Furniture): String = piece match {
case a: Couch => "Lie on the couch"
case b: Chair => "Sit on the chair"
}
Also works on types (classes)
public static double ComputeAreaModernSwitch(object shape)
{
switch (shape)
{
case Square s:
return s.Side * s.Side;
case Circle c:
return c.Radius * c.Radius * Math.PI;
case Rectangle r:
return r.Height * r.Length;
default:
throw new ArgumentException(
message: "shape is not a recognized shape",
paramName: nameof(shape));
}
}
C# (not only FP langs)
func do(i interface{}) {
switch v := i.(type) {
case int:
fmt.Printf("Twice %v is %v\\n", v, v*2)
case string:
fmt.Printf("%q is %v bytes long\\n", v, len(v))
default:
fmt.Printf("I don't know about type %T!\\n", v)
}
}
Go has something for types
let x = Some(5);
let y = 10;
match x {
Some(50) => println!("Got 50"),
Some(y) => println!("Matched, y = {:?}", y),
_ => println!("Default case, x = {:?}", x),
}
Rust
num : Int
num = 1
result : String
result =
case num of
1 -> "one"
2 -> "two"
_ -> "other"
`case x of` in Elm and Haskell
map :: (a -> b) -> [a] -> [b]
map _ [] = []
map f (x:xs) = f x : map f xs
Haskell also supports pattern matching on the function parameters.
Combined with destructuring it results in very declarative functions.
(Yup it's the actual Haskell's map implementation)
type alias State = Int
type Action = Inc | Dec
reducer : State -> Action -> State
reducer state action =
case action of
Inc -> state + 1
Dec -> state - 1
reducer 1 Inc -- 2
reducer 1 Dec -- 0
We can also pattern match on Algebraic Data Types
(i.e. custom types / union types)
type alias State = Int
type Action
= Inc
| Dec
| Add Int
reducer : State -> Action -> State
reducer state action =
case action of
Inc -> state + 1
Dec -> state - 1
Add x -> state + x
reducer 1 Inc -- 2
reducer 1 Dec -- 0
reducer 1 (Add 10) -- 11
reducer 1 (Add -10) -- -9
export default Split
This is the end goal !!!
This is all cool but we write JavaScript !!!
Perfect gif:
Nicki Minaj === JavaScript
tc39/proposal-pattern-matching
PRETTY PLEASE !!!
const res = await fetch(jsonService)
case (res) {
when {status: 200, headers: {'Content-Length': s}} -> {
console.log(`size is ${s}`)
}
when {status: 404} -> {
console.log('JSON not found')
}
when {status} if (status >= 400) -> {
throw new RequestError(res)
}
}
<Fetch url={API_URL}>
{props => case (props) {
when {loading} -> <Loading />
when {error} -> <Error error={error} />
when {data} -> <Page data={data} />
when _ -> throw new Error('badmatch')
}}
</Fetch>
So, what do you guys think about this ?
Useful or not ???
Any thoughts ...
It seems cool! So what can we do now?
JS/TS don't support pattern matching "per se" ...
BUT !!!
const fn = R.cond([
[R.equals(0), R.always('water freezes at 0°C')],
[R.equals(100), R.always('water boils at 100°C')],
[R.T, t => `nothing special happens at ${t}°C`],
])
fn(0) // => 'water freezes at 0°C'
fn(50) // => 'nothing special happens at 50°C'
fn(100) // => 'water boils at 100°C'
Yeah, Ramda, of course
// static/js/ui/Chip.js
const createIcon = R.cond([
[React.isValidElement, R.identity],
[R.is(String), name => <SvgIcon name={name} />],
[R.is(Object), props => <SvgIcon {...props} />],
[R.T, () => null],
])
BUT !!!
- NOT type safe
- Already useful, but still limited
- And awkward ...
Discriminated Unions
TypeScript gives use the tools to leverage the
type system to implement pattern matching
import match from '@housinganywhere/match'
type Variant =
| 'success'
| 'danger'
| 'warning'
const variantColor = match<Variant, string>({
success: () => 'green',
danger: () => 'red',
warning: () => 'yellow',
})
Et voilà !
type Matcher<T extends string, R> = { [K in T]: (k: K) => R };
const match = <T extends string, R = void>(m: Matcher<T, R>) => (t: T) => m[t](t);
Yeah, implementation is that simple !!!
And it gives EXHAUSTIVENESS !!!
import { wildMatch } from '@housinganywhere/match';
type Vowels = 'a' | 'e' | 'i' | 'o' | 'u';
const isA = wildMatch<Vowels, string>({
a: () => 'Yay!',
_: (v) => `Nope, "${v}" is not "a"`,
});
isA('a') // => 'Yay!'
isA('e') // => 'Nope, "e" is not "a"'
isA('u') // => 'Nope, "u" is not "a"'
This one is not published yet ...
What about the name ?
type PartialMatcher<T extends string, R> =
{ [K in T]?: (k: K) => R } & { _: (t: T) => R; };
const wildMatch = <T extends string, R = void>(m: PartialMatcher<T, R>) => (t: T) => {
const f = m[t];
if (f) {
return f(t);
}
return m._(t);
};
Even the `wildMatch` one is very simple!
type PayoutTypes = 'iban' | 'bank' | 'paypal'
const PayoutMethod = ({ payoutMethod, payoutType }) =>
<div>
{match<PayoutTypes, React.ReactNode>({
iban: () => (
<IbanMethod method={payoutMethod} isNew={!payoutMethod} />
),
bank: () => (
<BankMethod method={payoutMethod} isNew={!payoutMethod} />
),
paypal: () => (
<PaypalMethod method={payoutMethod} isNew={!payoutMethod} />
),
})(payoutType)}
</div>
`match` and `wildMatch` work on
enums and string unions
Still limiting !!!
What about pattern matching on data ?
`wildMatch` -> idea -> data in cases
Code time !!!
match<'foo' | 'bar'> // states
match<{ name } | { email }> // data
What else can we do ???
Can we combine them somehow ???
type RemoteData<D, E> =
| NotAsked
| Loading
| Success<D>
| Failure<E>
One use case would be states/status
of data that we fetch
parametrize -> generic data
import { RemoteData, cata } from 'remote-data-ts'
const renderArticle = cata<Article, string, React.ReactNode>({
notAsked: () => <Empty />,
loading: () => <Spinner />,
success: (article) => <Article {...article} />,
error: (msg) => <Msg variant="danger">{msg}</Msg>,
})
renderArticle(RemoteData.notAsked())
renderArticle(RemoteData.loading())
renderArticle(RemoteData.of({ title: 'Foo' }))
renderArticle(RemoteData.failure('404 Not found'))
RemoteData does the wrapping, etc.
Provides helpers to match each cases
and transform the content (only `Success`)
Map (map, then)
// (A -> B) -> RD<A, E> -> RD<B, E>
// (A -> B) -> A[] -> B[]
// Promise<A> -> (A -> B) -> Promise<B>
<A, E, B>(fn: (d: A) => B) => (rd: RemoteData<A, E>) => RemoteData<B, E>;
Chain (then, flatMap)
// (A -> B[]) -> A[] -> B[]
// (A -> RD<B, E>) -> RD<A, E> -> RD<B, E>
// Promise<A> -> (A -> Promise<B>) -> Promise<B>
<D, E, R>(fn: (d: D) => RemoteData<R, E>) => (rd: RemoteData<D, E>) => RemoteData<R, E>;
Fold (withDefault)
<D, E, R>(fn: (d: D) => R) => (def: R) => (rd: RemoteData<D, E>) => R;
gillchristian/remote-data-ts
Example
- Declarative
- Avoid spreading logic
- Composable
- Extendable
- Safe
- Verbose implementation / boilerplate
export default Split
So ...
An Introduction to ADTs and Structural Pattern Matching in TypeScript
Pattern Matching with TypeScript
Pattern matching and type safety in TypeScript
Pattern Matching Custom Data Types in Typescript
I'm not the first one to talk about this
Of course !!!