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

Misc discussion thread #1

Open
harrysolovay opened this issue Oct 24, 2020 · 67 comments
Open

Misc discussion thread #1

harrysolovay opened this issue Oct 24, 2020 · 67 comments

Comments

@harrysolovay
Copy link

I've enjoyed our chats in TypeScript issues. I see you're also a "cuber" (cool simulator!). Gqx looks auspicious as well.

You don't list a name, Twitter handle, etc. anywhere on your GitHub profile or project manifests.

Perhaps this is an unusual request, but I'd love to know who is behind the screen & make sure to stay posted. If you'd prefer to stay anonymous, I completely understand & respect your wishes!

Kind regards

@tjjfvi
Copy link
Owner

tjjfvi commented Oct 27, 2020

I'm just a random developer who enjoys typescript, cubing, and 3d printing.

gqx is a (very unpolished) library I've been working on that aims to provide a type-safe, implementation-agnostic way to express, communicate, and instantiate GraphQL fragments. For example,

const authorFrag = Author.$(
  Author.id,
  Author.name,
);

const bookFrag1 = Book.$(
  Book.id,
  Book.title,
  Book.author.name,
);

const bookFrag2 = Book.$(
  Book.id,
  Book.author.$(authorFrag)
);

const bookFragCombined = Book.$(
  bookFrag1,
  bookFrag2,
);

gqx.query.getBook({ id: "abc" }, bookFragCombined).then(book => {
  book; // Hover
  /*
    (parameter) book: {
      __typename: "Book";
      id: string;
      title: string;
      author: {
        __typename: "Author";
        id: string;
        name: string;
      };
    }
  */
})

Playground Link (Very unresponsive to edits; I recommend throwing into a vscode tab)

It also spews out lots of generic info about the graphql schema (both in types and runtime objects), which I've used in a couple projects to type tangental aspects of the gql schema, like resolver types.

@harrysolovay
Copy link
Author

That looks beautiful! Have you ever experimented with TS-first approaches to schema definition as well?

@tjjfvi
Copy link
Owner

tjjfvi commented Oct 27, 2020

Thanks! If you're referring to type-level schema parsing, I haven't much. Though I very much like the concept theoretically, since it seems like they aren't going to support it, I think it might be more trouble than it's worth, especially since I prefer to have my schema in separate .graphql files in a folder; I just have a script set up to watch the folder and run gqx every time it changes.

gqx is actually the reason I moved to typescript from flow; I had been trying to implement the frag -> type for days in flow and had been unsuccessful (circular recursion had weird bugs), but I was able to open a typescript playground window and implement it in like an hour. I haven't looked back!

@tjjfvi
Copy link
Owner

tjjfvi commented Nov 4, 2020

@harrysolovay I thought this might interest you: https://github.com/dotansimha/graphql-typed-ast

@harrysolovay
Copy link
Author

I guess this is to be our unofficial chat :)

Apologies for the delay. Lots & lots of work.

That repo absolutely did interest me! It was in fact a catalyst for my current project. I've been building a spec-compliant type-level GQL parser, and those recursion limits are brutal. Can barely parse a 30 loc schema without hitting limits. Also, the lack of throw types (proposed here) makes parse errors a pain: one needs to create a spec-incompliant node kind to contain errors, recursively extract all error nodes from the final tree, and display them with shady tricks (["error message 1", "error message 2"] is not assignable to "false").

Those HKT hacks you've been sharing seem promising! I find it hard to imagine that Ryan's comments against type-level parsers will stick... it would be such a beautiful way to tie together languages in a single env. One way or the other, 4.1 will be of huge benefit to GraphQL-in-TS tooling. I can see you've been thinking about the same problem space. Let's post in this issue whenever we want feedback from one another.

What've you been hacking on? Any cool issues I should take a look at?

@harrysolovay
Copy link
Author

Also, any thoughts on this issue? I feel like we're still writing conditional types like cave people ;)

@tjjfvi
Copy link
Owner

tjjfvi commented Nov 4, 2020

@harrysolovay Yeah, I saw it the other day and I agree that it would be nice. I started to write up a comment for it, but it must have ended up unsent among my tabs 😄

@tjjfvi
Copy link
Owner

tjjfvi commented Nov 4, 2020

Oh, I didn't see your earlier comment.

Yeah, throw types are a must for complex recursive conditional types, though IMO they should be assignable like never, as that is what people use right now, and them being the "anti-any" messes up passing conditionals involving them to constrained type parameters.

The problem that I've been running into with the HKT hacks is that if you have an HKT that increases the depth by e.g. 10, you have a lot less depth to work with than one that increases the depth by e.g. 1. I've been thinking about if there would be a way for it to detect that it hit depth limit (sometimes if the recursive type is wrapped, the error doesn't propagate and just returns any), backtrack, and repeat, to get the maximum possible recursion depth, but I haven't tried implementing such yet.

I've recently been playing around on the discord with a ConvertibleTo type (best explained by an example), and have been trying to get typescript to recognize this "axiom":

type Axiom<A> = Assert<ConvertibleTo<A>, ConvertibleTo<ConvertibleTo<A>>>

Eventually I got to this, but it would be nice to be able to do it in a more general way.

Wrt interesting issues, I personally think microsoft/TypeScript#41370 is a pretty big type hole, though I may be biased :)

@harrysolovay
Copy link
Author

Wow! That is absolutely a hole. Surprised I haven't encountered this limitation before.

I like that ConvertibleTo type! That could come in handy for building libs promoting trait-style patterns in TS. Aka, instead of a subtype check, you'd want to ensure convertibility as a "bound".

... which has me thinking... we need conditional assignability, badly.

@tjjfvi
Copy link
Owner

tjjfvi commented Nov 4, 2020

Yeah, that's exactly what I'm using/developing it for :)

Yeah, we really do.

@tjjfvi
Copy link
Owner

tjjfvi commented Nov 4, 2020

Also useful for the trait-style lib is what I've been using in my project to handle plugins:

// Main library
declare global {
  namespace someLibrary {
    interface Conversions {
      aToB: Conversion<A["id"], B["id"]>
    }
  }
}

// In some plugin
declare global {
  namespace someLibrary {
    interface Conversions {
      myPlugin: {
        bToA: Conversion<B["id"], A["id"]>
        misc: (
          | Conversion<B["id"], C["id"]>
          | Conversion<X["id"], Y["id"]>
        )
      }
    }
  }
}

Playground Link

@harrysolovay
Copy link
Author

This might take me a little while to grok. Is myPlugin the "trait"? What is the mental model?

@tjjfvi
Copy link
Owner

tjjfvi commented Nov 4, 2020

That was an alternative for my:

type Conversions = 
  | Conversion<A["id"], B["id"]>
  | Conversion<B["id"], A["id"]>
  | Conversion<B["id"], C["id"]>
  | Conversion<X["id"], Y["id"]>

AFAIK, unions cannot have multiple declarations / be extended.

Instead, I use an interface, and then to form the union of all conversions, I deeply get the values of the interface:

type ConversionsUnion<C = someLibrary.Conversions> = {
  [K in keyof C]: C[K] extends Conversion<any, any> ? C[K] : ConversionsUnion<C[K]>
}[keyof C]

With this, plugins can add conversions by extending the global interface someLibrary.Conversions.

The keys are irrelevant, and just have to be unique so they don't conflict between plugins; I allow it to be deep just for easier namespacing.

@tjjfvi
Copy link
Owner

tjjfvi commented Nov 4, 2020

A, B, C, X, and Y are the different types, and the Conversions interface just represents the relationships between them.

@harrysolovay
Copy link
Author

I see! How do you––at the type-level––safeguard against plugins providing conflicting conversions for the same to types? (I'm assuming there's a runtime component which injects properties in the module being augmented?)

@tjjfvi
Copy link
Owner

tjjfvi commented Nov 4, 2020

Right now, I don't have any type-level safeguards against conflicting conversions; all I care about in the type system right now is whether or not some path exists from A to B, to determine if something of type A can be passed to something expecting type ConvertibleTo<B>.

Run time wise, I have a registry for the conversions, and then it essentially path finds from A to B; I haven't yet figured out how I'm handling two different conversions of the same from and to types.

(I'm assuming there's a runtime component which injects properties in the module being augmented?)

What do you mean?

@harrysolovay
Copy link
Author

This approach of yours is just spectacular. Hopefully it'll enable––at the very least––type-level GQL document parsing to work for production apps (even if the schema types still need be generated).

@harrysolovay
Copy link
Author

Is that why you chose to give the conversions keys? (so that users need explicitly select their desired conversion?)

@harrysolovay
Copy link
Author

Would you possibly be able to write out an end-to-end from the user's perspective? It's still looking quite foreign to me.

@harrysolovay
Copy link
Author

Did this comment of yours mean that the HKT solution is not viable for circumventing recursion limiting in building string type parsers?

@tjjfvi
Copy link
Owner

tjjfvi commented Nov 5, 2020

Is that why you chose to give the conversions keys? (so that users need explicitly select their desired conversion?)

No; the keys are completely irrelevant. The end user doesn't really care about conversions (except for library authors); for the most part, it Just Works™

Would you possibly be able to write out an end-to-end from the user's perspective? It's still looking quite foreign to me.

Some context: I'm writing a programmatic 3d modeler (similar to OpenSCAD, but less horrible better).

That's somewhat difficult, as the end user doesn't ever really care about the details of conversions etc. unless they are writing a lower level library, but here's an example of the logic.

// myModel.ts
import escad from "escad"
import { cube } from "@escad/cube"

export default () => {
  return escad
    .cube({ sideLength: 1, center: true })
    .translateZ(1)
    .sub(cube({ sideLength: 2, center: true }))
}

Ignoring chaining implementation details:

interface Cube extends Product { /* ... */ }
interface TransformationMatrix extends Product { /* ... */ }
interface Mesh extends Product { /* ... */ }
interface BspNode extends Product { /* ... */ }
interface CsgOperation extends Product { /* ... */ }

type Transformation<P extends Product> = CompoundProduct<[TransformationMatrix, Cube]>
type CsgOperation<P extends Product, Q extends Product> = CompoundProduct<[CsgOperation, P, Q]>

cube // (args: CubeArgs) => Cube
translateZ // (z: number) => <P extends Product>(p: P) => Transformation<P>
sub // <P extends Product>(p: P) => <Q extends Product>(q: Q) => CsgOperation<Q, P>

import myModel from "./myModel"

const output = myModel() // CsgOperation<Transformation<Cube>, Cube>

type Assert<T, U extends T> = U;
type _0 = Assert<ConversionsUnion, (
  | Conversion<Cube, Mesh>
  | Conversion<Transformation<Mesh>, Mesh>
  | Conversion<BspNode, Mesh>
  | Conversion<Mesh, BspNode>
  | Conversion<CsgOperation<BspNode, BspNode>, BspNode>
)>
// Thus,
type _1 = Assert<ConvertibleTo<Mesh>, typeof output>
// Because:
// CsgOperation<Transformation<Cube>, Cube>
// CsgOperation<Transformation<Mesh>, Cube>
// CsgOperation<Mesh, Cube>
// CsgOperation<BspNode, Cube>
// CsgOperation<BspNode, Mesh>
// CsgOperation<BspNode, BspNode>
// BspNode
// Mesh

This might seem over complicated, but it's useful for a number of reasons:

  • I could make a 2D shape type, Shape, and add Conversion<Transformation<Shape>, Shape>
  • I could make a Beam type, and then
    • Add Conversion<Beam, Mesh>
    • Add Conversion<Transformation<Beam>, Beam>
    • This way, if someone is only transforming beams and isn't adding or subtracting them, their "beaminess" can be preserved, and additional information can be displayed on the client, like a hover showing the length and type of the beam
  • I could add volume information to the client
    • Add a Volume type
    • Add Conversion<Mesh, Volume>
    • Register a "statistic" for the Volume type on the client
    • Now, when the client receives a mesh, it will ask the server what types it can convert from Mesh. It finds Volume, and sees that a statistic is registered for a volume. It tells the server to convert the Mesh to a Volume, receives the volume, and displays the statistic
  • These conversions can be cached by the hash of the type, and saved from recomputing, so you can reuse various parts of the conversions that have been done before
  • Products can have different ancillary information added by multiple libraries, without conflicting with each other

Did this comment of yours mean that the HKT solution is not viable for circumventing recursion limiting in building string type parsers?

When I tried using it with the HKT recursion circumvention, it was 2589ing very quickly. I likely could have tweaked it to not 2589, but I wasn't really in the mood :)

@harrysolovay
Copy link
Author

Haha I know that feeling: it's gotta be a labor of love. The manic, unpredictable kind.

That's a very elegant experience you're working on! Once again, a lot to grok, but I feel the essence & I'd imagine users will appreciate the thoughtful typing experience. Extraordinary how much one can achieve with TypeScript's type system nowadays.

Out of all the coders I've met, the most sophisticated––when it comes to type systems––were once big into cubing. Funny coincidence I suppose. Or who knows, maybe some common experiences guided us to this niche.

If you do manage to circumvent 2589, please let me know. Although it looks like you have your hands full.

Also, feel free to add me as a reviewer if you ever want fresh eyes on your projects. Looking forward to seeing what escad becomes––even if it's purely research 👍

@tjjfvi
Copy link
Owner

tjjfvi commented Nov 5, 2020

Thanks! Once it gets further along and I clean it up a little, I'd love to hear your thoughts on it!

@tjjfvi
Copy link
Owner

tjjfvi commented Nov 5, 2020

Here a "sneaky type" I made; when typescript is checking its constraint with the type parameter $ unresolved, it thinks the constraint is F, but when it evaluates it with $ resolved, the type is V.

I'm unsure if this has any uses, but it's an interesting edge case.

@tjjfvi
Copy link
Owner

tjjfvi commented Nov 11, 2020

What do you think about a syntax like the below?

type [infer A, infer B] = [1, 2];

Where that would be equivalent to:

type A = [1, 2] extends [infer A, infer B] ? A : never;
type B = [1, 2] extends [infer A, infer B] ? B : never;

I think this would be especially useful in the context of microsoft/TypeScript#41470; the example I provided in there could be rewritten to:

type PromisifyAll<T> = {
  [K in keyof T & string as `${T}Async`]: {{
    type ((...args: infer OrigArgs) => void) = T[K];
    type [...infer Args, (error: any, result?: infer R) => void] = OrigArgs;
    type = (...args: SplitArgs["args"]) => Promise<SplitArgs["result"]>;
  }}
}

Which IMO looks very clean.

If it could be generic, you could nicely rewrite some common type aliases:

type [infer Head, ...infer Tail]<T extends any[]> = T;

As opposed to:

type Head<T extends any[]> = T extends [infer X, ...infer _] ? X : never;
type Tail<T extends any[]> = T extends [infer _, ...infer X] ? X : never;

@tjjfvi tjjfvi changed the title Who is "tjjfvi" Misc discussion thread Nov 11, 2020
@harrysolovay
Copy link
Author

I love the look of it! It's parallel to destructuring. Reminds me a bit of this issue, although I don't think the proposal was as strong as yours would be.

It would be useful for type parameters as well.

(example from issue)

type A = {
  B: {
    C: [
      {D: "E"}
    ]
  }
}

type X<{B: {C: {[infer D]}}} extends A> = D;

How would one deal with conditional destructuring / potentially-missing fields. Would it make sense for––upon a destructured element containing never––to opt for the next of the || sequence?... or is this a bit too much magic in your opinion? Plus, this relies on other new syntax:

type T = [1];
type U = [1, 2];

type [infer A, infer B] = T || U;

declare const a: A; // `1`
declare const b: B; // `2`

I don't quite understand how you would use this:

type [infer Head, ...infer Tail]<T extends any[]> = T;

Seems like you'd wanna do the following instead:

type HeadAndTail<T extends [infer Head, ...infer Tail]> = [Head, Tail];
type [infer Head, infer Tail] = HeadAndTail<["a", "b", ""c]>;

// or better yet, don't even use a utility

type [infer Head, ...infer Tail] = ["a", "b", ""c];

I like the anonymous type scoping proposal! As of today, I use namespaces to keep tidy... anonymous scopes could be of great help with that. Will comment in that thread soon.

Also apologies for not responding to your sneaky type! I hadn't yet had the time to understand it. Looks fascinating though.

@tjjfvi
Copy link
Owner

tjjfvi commented Nov 11, 2020

It would be useful for type parameters as well.

Hadn't thought of that, though I've always wanted some way to do that.

Though when I've thought about something like that, in my mind it has always been something like your HeadAndTail, rather than the X<{...} extends A> from the issue. IMO the one in HeadAndTail reads a bit cleaner (and has direct parity with conditionals), and having it both ways would seem inconsistent to me. Thus, I would probably rewrite the one from the issue as:

type X<_ extends A & {B: {C: [infer D]}}> = D;

How would one deal with conditional destructuring / potentially-missing fields. Would it make sense for––upon a destructured element containing never––to opt for the next of the || sequence?... or is this a bit too much magic in your opinion? Plus, this relies on other new syntax:

I had actually been thinking about this; I was initially thinking about : for parity with conditional types, but I think || makes more sense. For an initial proposal, it might make sense to leave that out, just for a narrower scope, but it might also be better received if it didn't make it implicitly never.

I think it could be nice if typescript threw an error if it couldn't prove that the infer pattern would match; if you wanted to to fall back to never, you could use ||:

type [infer A, infer B] = [1]; // Error
type [infer A, infer B] = [1] || never; // Ok, A & B are both never

I don't quite understand how you would use this:

type [infer Head, ...infer Tail]<T extends any[]> = T;

In my mind, that would be equivalent to:

type Head<T extends any[]> = T extends [infer Head, ...infer Tail] ? Head : never;
type Tail<T extends any[]> = T extends [infer Head, ...infer Tail] ? Tail : never;

While somewhat strange for that case, I think allowing generic support could be nice, to allow something like

type { x: infer X }<T> = Y<T>;

Instead of

type X<T> = {{ type { x: infer X } = Y<T>; type = X }};

or

type X<T> = Y<T> extends { x: infer X } ? X : never;

But perhaps it's not clear enough from the declaration that X is generic.

Also apologies for not responding to your sneaky type! I hadn't yet had the time to understand it. Looks fascinating though.

No problem! I probably should have given some explanation of it. Essentially, I made Wrap/Unwrap such that Unwrap<Wrap<A> resolves to A, but Unwrap<Wrap<A> & Wrap<B>> resolves to B. Then, when typescript is traversing Sneaky with $ unresolved, it decides that the constraint of ($ extends infer _ ? Wrap<V> : never) is unknown (idk why), and then Unwrap<Wrap<F> & unknown> resolves to F. However, when $ is resolved, it becomes Unwrap<Wrap<F> & Wrap<V>>, which resolves to V.

type Sneaky<$, V=$, F=never> = Unwrap<(Wrap<F> & ($ extends infer _ ? Wrap<V> : never))>;

@harrysolovay
Copy link
Author

harrysolovay commented Nov 12, 2020

That's quite an edge case you've found! Very cool!

Unrelated:

Since 4.0, I'm wondering when stricter generator types will happen. Generator signatures should IMO contain the ordered sequence of yields, next args, & return. This would enable library developers to create type-safe generator-based experiences, wherein the user supplies a to-be-type-checked generator.

For instance, let's say I want to mimic inheritance: a generator would allow me to yield the base class and super props, and then––after a behind-the-scenes super call––pass a scope back to the generator (wrapping the generator as a constructor). For instance:

import {toConstructor, useBase} from "generator-to-constructor";

// To inherit from:

interface CowProps {
  spotted: boolean;
}

class Cow {
  spotted;

  constructor(props: CowProps) {
    this.spotted = props.spotted;
  }

  moo() {
    console.log("MOOO!");
  }
}

// Synthesizing a constructor:

interface SmartCowProps extends CowProps {
  name: string;
}

const SmartCow = toConstructor(function*(props: SmartCowProps) {
  const scope = yield useBase(Cow, {spotted: props.spotted});

  scope.moo();

  return {
    name: props.name,
  }
});

You could even allow useBase to accept and––under-the-hood––wrap other generators.

import {toConstructor, useBase, Instance} from "generator-to-constructor";

interface CowProps {
  spotted: boolean;
}

function* Cow(props: CowProps)  {
  yield; // doesn't inherit anything

  return {
    spotted: props.spotted,
    moo() {
      console.log("MOOO!");
    },
  }
}

interface SmartCowProps extends CowProps {
  name: string;
}

function* SmartCow(props: SmartCowProps) {
  const scope = yield useBase(Cow, {spotted: props.spotted}); // essentially a `super` call

  scope.moo();

  return {
    name: props.name,
  }
};

const smartCow = Instance(SmartCow, {
  spotted: true,
  name: "Rick Ross",
});

Type-checking this would be crucial, as we're expecting a single yield containing a constructor and that constructor's props, and then a return which is to be instance members.

I'm curious to hear your thoughts on generator strictness, as well as the above.

Also––I feel like this is a topic you'd enjoy: any ideas about what a macros system for TS would look like? Was sifting through issues and though of maybe submitting a proposal in this one.

@harrysolovay
Copy link
Author

This is unfortunate

@harrysolovay
Copy link
Author

Ahh, ya got me ;)

@harrysolovay
Copy link
Author

Gave ya a shoutout in this article that I just published.

@tjjfvi
Copy link
Owner

tjjfvi commented Dec 2, 2020

That was a fun read! Sorry to ruin your theory, but I am not Elvis on a private island 😆 . How did the SafeTail utility prevent 2589? Do you happen to have a repo/playground of the final code in your article that I could take a look at?

Also, I realized a way type level parsing might be able to perf well (assuming it didn't 2589): build mode with project references will only build it when it changes. (Though thinking about it now it might not actually instantiate the type before it generates the d.ts)

Reading through your article gave me an idea for possibly circumventing 2589 more legibly. I'll report back soon 🙂 .

(Edit: just sticking this here so I can find it)

@tjjfvi
Copy link
Owner

tjjfvi commented Dec 2, 2020

Not sure what to say, so I'm just gonna link this and let it speak for itself. 😄

@harrysolovay
Copy link
Author

Not Elvis... dang ;)

The project references idea is interesting. Would every sub-parser be its own package? And how would this be more performant?

I'm not so sure I fully understand the link. Are you suggesting there's an approach that would make type-level GraphQL parsing feasible?

Also, I'm glad you enjoyed the read!

@tjjfvi
Copy link
Owner

tjjfvi commented Dec 3, 2020

In my mind, the project references would be more performant because dependents only get built on change, but thinking about it now, the generated d.ts might leave the ParsedAST type as Parse<Src> and not actually instantiate the type.


I was trying to demonstrate a recursion limit hack; given a tail recursive type like below:

// type Final = "x".repeat(1e3)
type LotsOfX<T extends string = ""> = T extends Final ? T : LotsOfX<`x${T}`>;

You can transform it into:

type _LotsOfX<T extends string = ""> = T extends Final ? Result<T> : Defer<LotsOfX<`x${T}`>>;
type LotsOfX<T extends string = ""> = x1e9<_LotsOfX<T>>;

// "x".repeat(1e3)
type X = LotsOfX;

In that example, it went through 1000 iterations of 'T = x${T}', then 999999000 (1e9-1e3) no-op iterations, and then returned the final value of T.

In retrospect, this is probably a better example.

At that point, the challenge is to make the Parse type tail recursive. I modified my generic Parse type from this comment, and it works (imvho) spectacularly:

namespace Patterns {
  export type Leaf = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9";
  export type Element = Leaf | Group;
  export type Group = ["(", Element[], ")"]
}

// Parses without 2589
type X = Parse<Patterns.Element, "((1)((((0))))(1(0101(((1(((((1)))))()()()(0001234567890()0000)1)))01010((10)1)010)1))">

Playground Link

Parse essentially takes a nested union of all possible matches, and recursively narrows it down to match the Src string.

I'm going to be modifying it to support a mapping at the end so that it can produce a normal-looking ast.

@harrysolovay
Copy link
Author

With a bit more nesting, we get the following.

Screen Shot 2020-12-03 at 1 23 22 PM

Writing these type-level parsers is fun... but it's also such a limited process. That conditional assignability syntax would make a difference to this process. Even if we had it though, a plugin system for intrinsics might be preferable. While I love the problem of type-level parsing, it doesn't seem like we're quite there yet :/

On another note, I'm surprised no one else has chimed in on the documentation utility type discussion. Seems like it'd be an extraordinary addition to the language.

& how is the 3D modeling lib coming along?

@tjjfvi
Copy link
Owner

tjjfvi commented Dec 3, 2020

With a bit more nesting, we get the following.

😞

Even if we had it though, a plugin system for intrinsics might be preferable.

Agreed.

& how is the 3D modeling lib coming along?

It's going well. I'm currently procrastinating making the front-end, but the core model generation / conversion stuff is mostly done. I have yet to put it through a real stress test, however.

@tjjfvi
Copy link
Owner

tjjfvi commented Dec 3, 2020

Update: stress test did not go well. Will need further testing 😆

I think it might have been in an infinite loop, but maybe my algorithm requires 3.5+ million iterations 🤷

@harrysolovay
Copy link
Author

Haha sorry to hear. Hopefully you don't let it loose on an AWS lambda function with self-provisioning permission ;)

Looking forward to seeing / hearing about whatever it becomes

@tjjfvi
Copy link
Owner

tjjfvi commented Dec 4, 2020

This inconsistency is annoying.

@harrysolovay
Copy link
Author

Very interesting! Likely the same issue where inference breaks upon unwrapping known types in conditionals. Probably worth an issue in TypeScript. Is there any reason that this behavior would be desirable?

@tjjfvi
Copy link
Owner

tjjfvi commented Dec 7, 2020

Call utility type / HKTs :)

I think that's a separate issue; there it doesn't infer the constraint of the type (though if you ignore the error, it works). With the one I linked, it doesn't infer it properly in the end result either. I guess the inference of conditionals is separate from inference of function arguments?

@harrysolovay
Copy link
Author

If you got your hands on a Call utility type... I have a feeling you'd be sharing some wild experiments. I'd very much like to see that. Although I'd personally prefer a syntactical solution / type lambdas, as opposed to an intrinsic Call utility type.

type X<A> = <B> = [A, B];

type Y = X<number>; // <B> = [number, B];

type Z = y<string>; // [number, string];

@tjjfvi
Copy link
Owner

tjjfvi commented Dec 7, 2020

Although I'd personally prefer a syntactical solution / type lambdas, as opposed to an intrinsic Call utility type.

A Call utility type would also be useful for some fp type stuff, so I'd advocate for both :)

@harrysolovay
Copy link
Author

Something like this?:

type Call<
  G extends <X> = any,
  A extends (G extends <infer X> ? X : never)
> =
  G<A>;

@tjjfvi
Copy link
Owner

tjjfvi commented Dec 8, 2020

IMO the <X> = Y syntax is kinda confusing; is G extends <X> = any parsed as G extends (<X> = any) or G extends (<X>) = any (and is that last syntax even valid?)?

I would probably opt for something like

type CallHKT<
  G extends <X extends never> => unknown, // These could also be any, but this demonstrates the variance
  A extends (G extends <X extends infer X> => unknown ? X : never)
> = G<A>;

Which would IMO reduce that confusion.

But, no, when I say "a Call utility type would also be useful for some fp type stuff", I mean something like Call<F extends Callable, A extends Parameters<F>>, which could be used to make something like this:

type Callable = (...args: any[]) => any;
type CurryOnce<T extends Callable, F extends <X> => any = <X> => X> = 
  | ([] extends Parameters<T> ? () => F<Call<T, []>> : never)
  | (
    <A extends HeadArr<Parameters<T>>>(...args: A) =>
      F<
        <B extends Tail<Extract<Parameters<T>, [...A, ...any[]]>>>(...args: B) =>
          Call<T, [...A, ...B]>
      >
  )
type Curry<T> = (
  T extends Callable
    ? T extends () => infer R
      ? R
      : CurryOnce<T, Curry>
    : T
)
declare const curry: <F extends Callable>(f: F) => Curry<F>;

declare const make3Tuple: <A, B, C>(a: A, b: B, c: C) => [A, B, C];
// <A>(a: A) => <B>(b: B) => <C>(c: C) => [A, B, C]
const curriedMake3Tuple = curry(make3Tuple);

@harrysolovay
Copy link
Author

I see what you're saying! Such utilities for partial application would enable HOTs as well if I'm not mistaken?

@tjjfvi
Copy link
Owner

tjjfvi commented Dec 8, 2020

Yes; with a Call utility, you could use <T>(t: T) => Whatever<T> as an HKT. That's one of the main reasons why I want such a utility :)

@harrysolovay
Copy link
Author

I wanted to check in. I hope you're enjoying this holiday season. What've you been hacking at lately?

Thought I'd update you as well: I'm switching teams. Joining Alexa, where I'll be working on a programming language for the conversation-modeling use case. You and I have had great conversations about language, and I'd love to get your feedback on design decisions down the line. It's all still very new to me, and I haven't started yet, so I can't detail the work with any certainty... but I'm excited!

Meanwhile, I've been writing an absurdly-fast GraphQL-to-TS-declaration generator in Rust. This project is helping me learn the language. The challenge of minimizing memory footprint in Rust feels similar to that of maximizing type-safety in TS.

Speaking of TS, I've been very insistent on generic type safety for tagged templates––love to hear your thoughts on the issue.

& yea, that sums it up. This new year is looking up man 💯

@tjjfvi
Copy link
Owner

tjjfvi commented Jan 4, 2021

I wanted to check in. I hope you're enjoying this holiday season. What've you been hacking at lately?

Ditto! Not too much, mostly advent of code (1st on the TypeScript Discord leaderboard 😄 ) and migrating my Rubik's cube simulator into unity, with the intention of adding VR support.

Thought I'd update you as well: I'm switching teams. Joining Alexa, where I'll be working on a programming language for the conversation-modeling use case. You and I have had great conversations about language, and I'd love to get your feedback on design decisions down the line. It's all still very new to me, and I haven't started yet, so I can't detail the work with any certainty... but I'm excited!

That sounds neat; I'm looking forward to hearing more about it!

Speaking of TS, I've been very insistent on generic type safety for tagged templates––love to hear your thoughts on the issue.

Huh, I never fully realized there wasn't generic support there. I'll probably write up something in the next day or two, though I think your comments cover a bit of what I'd say :)

@tjjfvi
Copy link
Owner

tjjfvi commented Jan 4, 2021

Meanwhile, I've been writing an absurdly-fast GraphQL-to-TS-declaration generator in Rust. This project is helping me learn the language. The challenge of minimizing memory footprint in Rust feels similar to that of maximizing type-safety in TS.

Neat! Rust has been a language I've been meaning to learn for a while; if you're ever looking for another set of eyes, feel free to send me a link :)

@harrysolovay
Copy link
Author

I'll absolutely share it with you when it's ready.

Just joined the TypeScript discord. Advent of Code looks interesting! What specifically have you been diggin' into with regards to Advent of Code?

@tjjfvi
Copy link
Owner

tjjfvi commented Jan 5, 2021

Cool! I'm T6 on there.

I completed all the challenges (you can see my solutions at tjjfvi/aoc), but the most interesting one is probably my solution for day 19, part 2. I can go into more detail later, but it uses a generic, type-safe, generator-based parser I made that I'm quite fond of. Here's an example of a JSON parser using it (not very polished/readable). It doesn't make much use of its type-safety (it casts it to any because it's spitting out raw JSON); I can make a better example later if you're interested.

@harrysolovay
Copy link
Author

This is unbelievably cool! I don't have the time right now to dig in fully, but it looks fancy! When you say it's type-safe... you don't mean you're getting the parsed result as a literal type, do you?

@tjjfvi
Copy link
Owner

tjjfvi commented Jan 7, 2021

Thanks!

I don't have the time right now to dig in fully

Not a problem! I hope you don't mind if I explain it some here anyway :)

It's based off of generator functions (aptly named Parsers) that take in a Loc (string to be parsed & current index in the string) and yield the parsed value (AST or whatever else the result (often represented by the generic R) is), and the new Loc. You start with very basic parsers, like string() and regex(), which can then be composed with things like or and concat, which in turn return new generators.

Here's a simple example:

const leaf = regex(/\d/);
const group = concat(string("("), multiple(() => element), string(")"));
const element = or(leaf, group);

This is great, but the return type of this isn't very useful, as it would look something like ["(", [["0"], ["1"], ["(", [["2"]], ")"]]]. We can transform it via map:

// We use ([n]) to unwrap the RegExpExecArray
const leaf = map(regex(/\d/), ([n]) => +n);
const group = map(concat(string("("), multiple(() => element), string(")")), ([,els]) => els);

group is pretty noisy; wrapping an element with throwaway nodes is a common use case, so we define surround:

const surround = <R,>(before: Parser<unknown>, parser: Parser<R>, after: Parser<unknown>) =>
  map(concat(before, parser, after), ([,v,]) => v)

const group = surround(string("("), multiple(() => element), string(")"));

Right now, this will give a type error, since typescript can't determine the type of circular variables. We have to give element an explicit type annotation:

type Element = number | Element[];
const element: Parser<Element> = or(leaf, group);

In the end, it looks like this (surround is defined in the pseudo-lib):

type Element = number | Element[];
const leaf = map(regex(/\d/), ([n]) => +n);
const group = surround(string("("), multiple(() => element), string(")"));
const element: Parser<Element> = or(leaf, group);

console.log(parse(element, "(01234(5)(6()7)89(10))")) // [0, 1, 2, 3, 4, [5], [6, [], 7], 8, 9, [1, 0]]

Some more examples: JSON parser (cleaned up), expression parser, infix to postfix (a minor adaption of the expression parser). All of these do the processing in maps, and skip an AST step, but you could also use this to construct an AST. I just couldn't think of any examples that were simple enough to be a good example but were complex enough to warrant an AST.

When you say it's type-safe... you don't mean you're getting the parsed result as a literal type, do you?

No; I wish :). I mean that if you use it to construct an AST (or even my barebones Element type), it typechecks all of the levels of the AST generation, often without much explicit type annotation.

@harrysolovay
Copy link
Author

Hey! Would love to check in. Are you around for a call sometime?

@tjjfvi
Copy link
Owner

tjjfvi commented May 5, 2021

I'm on discord a bit; I've sent you a friend request there if you want to chat more synchronously 🙂

@harrysolovay
Copy link
Author

Sounds good! I can't seem to find the request––what's your username?

@tjjfvi
Copy link
Owner

tjjfvi commented May 5, 2021

I'm T6#2591

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

No branches or pull requests

2 participants