unsplash / sum-types

Safe, ergonomic, non-generic sum types in TypeScript.
https://unsplash.github.io/sum-types/
MIT License
42 stars 2 forks source link

High-Level API questions #42

Closed anthonyjoeseph closed 2 years ago

anthonyjoeseph commented 2 years ago

Hello! Big fan of the work you're doing here! I apologize in advance if these questions have already been answered elsewhere

1) (from the readme)

...in our testing we've found that [support for generics] introduce unsafety into pattern matching...

could you provide example(s) of this unsafety? Maybe using ts-adt?

2) What is the purpose of making these sum-types serializable into tuples?

It seems like the major improvement is that it eliminates the need for a named 'discriminant' (e.g. _tag for fp-ts types, type for redux Actions, _type by default in ts-adt)

Does that provide some safety advantage relating to question 1)?

samhh commented 2 years ago

Hey there!

could you provide example(s) of the unsafety?

It's been quite a while since we looked at this, but there's a branch with a failing test here. If memory serves the issue often presented itself with nested pattern matches like this:

match({
  X: match({
    Y: () => 'this will not be inferred correctly',
  })
})

If there's some way to make this typesafe I'd love to support generic/polymorphic pattern matching.

It seems like the major improvement is that it eliminates the need for a named 'discriminant'

It's primarily to push consumers towards more idiomatic solutions than accessing the discriminant manually. If you do x.tag in fp-ts there's no sort of guide to warn you that it's unidiomatic.

We do still use a private symbol as a discriminant under the hood but the consumer should be thinking in terms of sum types a la Haskell rather than in terms of how it's technically implemented in TypeScript.

What is the purpose of making these sum-types serializable into tuples?

It's just the least opinionated way to hold two pieces of data.

If you'd prefer to serialise to an object you can define your own serialisation function as follows. I'm not sure how to avoid the assertion:

// We don't export these as generally you'd use tuple serialisation or something like the io-ts bindings.
type Tag<A> = A extends Sum.Member<infer B, any> ? B : never;
type Value<A> = A extends Sum.Member<any, infer B> ? B : never;

// The conditional type distributes over the union members.
export type SerializedO<A> = A extends Sum.AnyMember
  ? { tag: Tag<A>; value: Value<A>; }
  : never

// Inferrence is a bit iffy, need to be pointful to access `A` in the assertion.
export const serializeToObject = <A extends Sum.AnyMember>(x: A): SerializedO<A> =>
  pipe(x, Sum.serialize, ([tag, value]) => ({ tag, value }) as SerializedO<A>)

type Weather = Sum.Member<'Sun'> | Sum.Member<'Rain', number>
declare const w: Weather
serializeToObject(w) // { tag: "Sun"; value: null; } | { tag: "Rain"; value: number; }
anthonyjoeseph commented 2 years ago

got it - so this is less of a utility for working with existing discriminated unions, and more of a soup-to-nuts sum type library that aims to mirror Haskell as closely as possible, encapsulated to prevent unidiomatic usage. Does that sound right?

I'll investigate that nested pattern matching error on my own - I'd love to help out!

samhh commented 2 years ago

That's exactly it, and please do! :+1: