gvergnaud / hotscript

A library of composable functions for the type-level! Transform your TypeScript types in any way you want using functions you already know.
3.38k stars 57 forks source link

feat(Parser) Add monadic parser combinator #79

Open gvergnaud opened 1 year ago

gvergnaud commented 1 year ago

Since one type-level parser implementation is worth a thousand words, here is how to implement a JSON parser:

type JSON = P.OneOf<[NullLit, StrLit, NumLit, Obj, Arr]>;

type NumLit = P.Number;

type NullLit = P.Do<[P.Literal<"null">, P.Ap<Constant<null>, []>]>;

type StrLit = P.Do<
  [
    P.Literal<'"'>,
    P.Let<"key", P.Word>,
    P.Literal<'"'>,
    P.Ap<Identity, ["key"]>
  ]
>;

type Obj = P.Do<
  [
    P.Literal<"{">,
    P.Optional<P.WhiteSpaces>,
    P.Let<"key", StrLit>,
    P.Optional<P.WhiteSpaces>,
    P.Literal<":">,
    P.Optional<P.WhiteSpaces>,
    P.Let<"value", JSON>,
    P.Optional<P.WhiteSpaces>,
    P.Literal<"}">,
    P.Ap<
      Compose<[Objects.FromEntries, Objects.Create<[arg0, arg1]>]>,
      ["key", "value"]
    >
  ]
>;

type CSV = P.Do<
  [
    P.Let<"first", JSON>,
    P.Optional<P.WhiteSpaces>,
    P.Let<
      "rest",
      P.Optional<
        P.Do<[P.Literal<",">, P.Optional<P.WhiteSpaces>, P.Return<CSV>]>,
        []
      >
    >,
    P.Ap<Tuples.Prepend, ["first", "rest"]>
  ]
>;

type Arr = P.Do<
  [
    P.Literal<"[">,
    P.Optional<P.WhiteSpaces>,
    P.Let<"values", CSV>,
    P.Optional<P.WhiteSpaces>,
    P.Literal<"]">,
    P.Ap<Identity, ["values"]>
  ]
>;

And here is how to use it:

type res1 = Call<JSON, '[{ "key": "hello" }]'>;
//    ^? ["", P.Ok<[{ key: "hello" }]>] 

type res2 = Call<JSON, '["a", "b"]'>;
//    ^? ["", P.Ok<["a", "b"]>] 

type res3 = Call<JSON, '["a", null]'>;
//    ^? ["", P.Ok<["a", null]>] 

type res4 = Call<JSON, "null">;
//    ^? ["", P.Ok<null>] 

type res5 = Call<
  //  ^? ["", P.Ok<[null]>] 
  Arr,
  "[null]"
>;

type res6 = Call<
  //  ^? ["", P.Ok<[1, 2, 3]>] 
  CSV,
  "1 ,2  ,  3"
>;

type res7 = Call<
  //  ^? ["", P.Ok<[1, null, 3]>] 
  CSV,
  "1 ,null  ,  3"
>;

type res8 = Call<
  //  ^? ["", P.Ok<[1, null, "str"]>] 
  CSV,
  '1 ,null  ,  "str"'
>;

type res9 = Call<
  //  ^? ["", P.Ok<[1, null, "str"]>] 
  JSON,
  '[1 ,null  ,  "str"]'
>;

type res10 = Call<
  //  ^? ["", P.Ok<[1, { a: { b: "hello" } }, "str"]>] 
  JSON,
  '[1, { "a": { "b": "hello" } },  "str"]'
>;
ecyrbe commented 1 year ago

Owesome! 🎉

I love the Do Let Ap utils. makes everything simple to grasp, i think that nothing would prevent my implementation to also have it since it's just adding the ability to Sequence parsing to take a Fn that returns a Parser.

Our implementations are almost the same. The difference is only :

I think the debate wether introspection is a nice or not is not my concern, i could let this feature go away. i personnally think it allows easier error handling out of the box. But maybe adding mapError to customize errors is fine.

And about object vs tuple as the returned type of the parser, i prefer readbility for the user. I think object is also faster. But maybe i'm wrong.

Here are some minor things that are easily fixable :

But instead of diverging more. I think we should try to merge our implementations. I don't like spliting the effort here since both are almost identical.