-
Notifications
You must be signed in to change notification settings - Fork 3
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
Variant 3.0 #17
Comments
Instead of calling a separate function, simply call the helper inside the match expression. declare const animal: Animal;
const aniType = animal.type; // type: 'cat' | 'dog' | 'snake';
const result = match(onLiteral(aniType), {
cat: ({type}) => type,
default: constant(6),
}); |
I've put effort into a new generics implementation. The interface is much improved! Instead of needing to call a separate function and store the object in two different variables, we can simply use the helper function By that same token, ExamplesOption<T>The option (a.k.a. maybe) type is very simple to define and use. const Option = variant(onTerms(({T}) => ({
Some: payload(T),
None: {},
})));
type Option<T, TType extends TypeNames<typeof Option> = undefined>
= GVariantOf<typeof Option, TType, {T: T}>;
const num = Option.Some(4);
const name = Option.Some('Steve'); As you might expect, name cannot be assigned to num, as the types of It is just as elegant to write the extract function: Note that I did not annotate the return type. TS will infer from the match statement. Tree<T>Trees will need to be defined type-first like all recursive variants. const Tree = variant(onTerms(({T}) => {
type Tree<T> =
| Variant<'Branch', {payload: T, left: Tree<T>, right: Tree<T>}>
| Variant<'Leaf', {payload: T}>
;
return {
Branch: fields<{left: Tree<typeof T>, right: Tree<typeof T>, payload: typeof T}>(),
Leaf: payload(T),
}
}));
type Tree<T, TType extends TypeNames<typeof Tree> = undefined>
= GVariantOf<typeof Tree, TType, {T: T}>; but otherwise follow the same process. const binTree = Tree.Branch({
payload: 1,
left: Tree.Branch({
payload: 2,
left: Tree.Leaf(4),
right: Tree.Leaf(5),
}),
right: Tree.Leaf(3),
})
function depthFirst<T>(node: Tree<T>): T[] {
return match(node, {
Leaf: ({payload}) => [payload],
Branch: ({payload, left, right}) => {
return [payload, ...depthFirst(left), ...depthFirst(right)];
}
})
}
const [d1, d2, d3, d4, d5] = depthFirst(binTree);
expect(d1).toBe(1);
expect(d2).toBe(2);
expect(d3).toBe(4);
expect(d4).toBe(5);
expect(d5).toBe(3); |
Variant constructors and variants themselves now follow behavior that I want to call monadic, but I don't want to fight pedants, so let's say instead they work like Promises in that a variant is only ever one "level" deep. Now,
The most common way I expect this to come up is also the simplest. You can now override the friendly name and the underlying type by passing the const Animalish = variant({
dog: variation('DOG', fields<{name: string}>()),
cat: fields<{name: string, furnitureDamaged?: number}>(),
snake: (name: string, pattern = 'striped') => ({name, pattern}),
}), creates a version of |
The error reporting on the |
I'm wondering if there should be some consolidation between I know the builder pattern can be a little offputting, but I think its the superior API overall. |
Also with a builder pattern you can remove the "default" variant and rely on an |
Hello Michael! It's good to see you.
Funny you should say that. I've been spending time trying to consolidate the APIs, but at the moment my suspicion is that it won't work out so cleanly. First I tried to add a similar sort of I actually got it working by returning a
Hehe, yeah, I think you might have been there when Retsam complained about my little sample on discord. I guess to be fair, it is slightly less clean when used inline: {matcher(thing).when({
caseOne: _ => {},
}.complete()) {match(thing, {
caseOne: _ => {},
})}
I like the builder pattern too! It was definitely the most sensible way to support multi-match which gives it a leg up on the competition (and the regular match). Lack of brevity aside, I think the downside of the builder is that when using an object in the However, the on 'default'
That's true! Has
I hesitate to post this as a suggested improvement because it's not very different from the matcher, but I'm playing around with a I hope some of that was interesting. Thanks for your comments! I may be overlooking some options, I'd welcome any suggestions. |
Hey I have been using your experimental code shared in #12 for some time now and it has been working great. I have been thinking tho that it might be great if we could get a "point free" / curryed version of the match function so that we can simplify promise callbacks. e.g: const { match } = matchImpl(`kind`);
export const matchKind = match;
const fetchUser = async (id: string): Promise<Response> => {
// ...
}
fetchUser("abc123").then(match({
"success": ({ user }) => {},
"error" ({error}) => {}
})) rather than fetchUser("abc123").then(resp => match(resp, {
"success": ({ user }) => {},
"error" ({error}) => {}
}) Thoughts? NOTE: Edited the example code because it was incorrect, see one of my posts below. |
Good timing @mikecann - I added a function called
prematch notes.
|
@paarthenon interesting! Really happy to see you are still working on this. So if I understand correctly I dont think my example above would be possible? I just relised my above example was incorrect. I have now edited it: const { match } = matchImpl(`kind`);
export const matchKind = match;
const fetchUser = async (id: string): Promise<Response> => {
// ...
}
fetchUser("abc123").then(match({
"success": ({ user }) => {},
"error" ({error}) => {}
})) I think what im looking for is the reverse of what you have so match function sig needs to look like: const match = (handler) => (object) => { ... } That way it can do this without having to create a "prematcher" fetchUser("abc123").then(match({
"success": ({ user }) => {},
"error" ({error}) => {}
})) |
It is possible, just not with that function. I got it working, I was actually writing the message but had to jump on a call. It's available now in const renamedAnimals = animalList.map(match({
cat: _ => ({..._, name: `${_.name}-paw`}),
dog: _ => ({..._, name: `${_.name}-floof`}),
snake: _ => ({..._, name: `${_.name}-noodle`}),
})); |
Sweeeet! Thanks :) |
I wanted to provide an update. I've been yanked away by my busiest few weeks in years, but there's still been some progress. I rewrote the matcher. Its code should be much easier to comprehend and contribute to. It's also the first instance of a class within the library. Fun. Improvements to
|
It's up! https://paarthenon.github.io/variant/docs/next/intro So far that's the only page that's been written, but I think it already does a better job of highlighting some of the features of this library than the old intro. The new sidebar has my planned structure for the documentation. As you can see, there's a lot more to go into than there was before. I've received feedback that certain features like recursive or generic variants felt "buried", and there were a number of things that were available in the library that I never properly documented. The longer table of contents reflects these adjustments. It will only grow as I flesh out the text. The code samples are superpowered with shiki-twoslash. Hover over any term to see the type information. The newest versions have smooth docusaurus support. This doesn't currently carry JSDoc comments but that might change in future (shikijs/twoslash#64). I'm using a modified version of rainglow's I'm also working on a demo project Kind of Super that will show how variant can integrate with a real-world stack and moderately interesting logic. This will be the basis of the tutorial. I'll keep making updates to the docs and quietly releasing them. I'll post here if there's a major section complete or new functionality update. I welcome any suggestions. p.s. I know the jokes are dumb. It's been a long week. |
Phenominal work again! I love the incredible amounts of effort you put into the docs, it really helps explain what the value in using Variant is. I hadnt heard of shiki-twoslash but its very impressive. Started following Kind of Super updates, looking forward to seeing it when its done :) |
Hey, I was just browsing through some of the 3.0 docs (great work btw) and noticed you are replacing Im okay with the change im just not sure on the name of the function Thoughts? |
I think that's a good point. I'm open to suggestions if you had something in mind. My gut reaction is a one letter change to |
Ye |
I've added Thank you for your feedback and support. |
I've redone the interface for match. The problem with the existing API is that the error messages suck. If you are missing a case in a match, you see something like this (thanks @m-rutter for bringing this up as an issue)
This is really obtuse and obfuscates The actual error, Which is buried in the specifics for overload one. The default keyword is a major red herring since it's the last thing the compiler mentions but has nothing to do with the problem - we aren't doing any partial matching. The new error is the simple message about missing properties that you would expect.
To achieve this I moved partial handling to a secondary function call, which removes the heavy overloads and allows TS to disambiguate. This is still as type safe as ever, thanks to types flowing through higher order functions. const furnitureDamaged = (animal: Animal) => match2(animal, partial({
cat: _ => _.furnitureDamaged,
default: _ => 0,
})); This makes it more opt-in, and creates room for other match functions. For example, const cuteName = match2(animal, table({
cat: 'kitty',
dog: 'pupper',
snake: 'snek',
})); Maybe this should be called lookup for parity with matcher, but I remember being happy when I deleted As the name might imply, this implementation is available for testing via p.s. This does retain the point-free overload. That one was sufficiently distinct. Updates have been a little bit slower recently due to a wrist injury. I'm seeing a doc this week, I'm hoping that will help. |
Really impressive what are you doing here! Awesome lib 👏🏻👏🏻 |
The regular
Waffling over Revisiting
|
very cool @paarthenon! I like the simplification. My only slight concern is what it does to TS type check time, hopefully not too bad :) BTW I just tried the v2.1 version of |
I compared the new implementation to the old one and didn't find a difference in a trial loop, but that will only check runtime performance. I'll check out the options for auditing type performance. In the meantime, I'll be testing before release with progressively larger unions to confirm that things won't suffer.
Try with |
Ah yes of course :) Thanks |
Hello folks, there's a new batch of changes. These should be available in I gave While I was there, I adjusted a couple of things. The overloads for const greekLetters = greeks.map(letter => matcher(letter)
.with({
alpha: _ => 'A',
beta: _ => 'B',
gamma:_ => 'Γ',
})
.complete()
); Now that these overloads no longer conflict, the The const animals = [
Animal.dog({name: 'Cerberus'}),
Animal.cat({name: 'Perseus', furnitureDamaged: 0}),
];
const isAnimalList = animals.map(isOfVariant(Animal)); I extended the new interface of match (favoring the helper functions) over to the The documentation has been improved. I fleshed out more of the "Inspection" page and wired up the API to use TypeDoc. It will now automatically update with the library doc comments. In my next updates, I'll experiment with categories in TypeDoc, and attempt to have more of the documentation (the organization and the matching sections) fleshed out. I hope to have things ready for a 3.0.0 alpha in the coming weeks. The "Kind of Super" updates will probably happen after that. I'm eager to do them, but I still have limited typing and I should probably focus on the core functionality first. |
Hey @paarthenon its been a long time since I have checked in. Hows progress on v3.0 ? |
Hey @mikecann thanks for stopping by. Progress on 3.0 was halted for some time. I ran into some serious difficulties with my health and have been unable to work on the project. In the past few weeks, I've started typing again. Now, I'm cautiously optimistic about my ability to return to things. I participated in a game jam this weekend, and while it was definitely taxing, the fact that it was possible speaks more to my recovery than anything else I've done. I actually used 3.0 as a core part of what I built this weekend and had a smooth experience. The essential functionality is likely ready for me to push to alpha. My standards were a little too high back when I expected to have an infinite road of typing in front of me. By essential functionality I mean variant creation (fields, payload, custom functions), matching, and so on. The things that have been holding me back have been:
I'm taking stock, seeing how important these things actually are, and we'll move forward soon. Another user has kindly offered their assistance with the project, and I'm creating more of these things as issues with information others can leverage so that I'm not the big bottleneck anymore. |
Awesome thanks @paarthenon for all of that. Sorry to hear about the health issues mate, that really sucks particularly when you seemed to be steaming ahead just wonderfully. Ye its up to you when you want to release v3.
I personally definately will require custom discriminator names because I like to use Your docs are definately one of the best bits of Variant so I think those and the basic matching are the most important things to me. No rush tho, health is number one priority :) |
Changes available in I've added a export const calculateIfBattleWasAbandoned = (reason: BattleFinishedReason): boolean =>
match(reason, withFallback({
timeout: () => true,
surrender: () => true,
attack: () => false,
}, () => false)) For export const calculateIfBattleWasAbandoned = (reason: BattleFinishedReason): boolean =>
matcher(reason)
.when(['timeout', 'surrender'], () => true)
.when('attack', () => false)
.complete({
withFallback: () => false
}) Ideally it would also be possible to use this sort of fallback handling alongside exhaustiveness checking. The export const calculateIfBattleWasAbandoned = (reason: BattleFinishedReason): boolean =>
matcher(reason)
.when('attack', () => false)
.remaining({ // both properties required
timeout: () => true,
surrender: () => true,
})
.complete({
withFallback: () => false
}) |
Also some IDEs are able to auto-fill the objects based on the types of the keys :) Nice work on this, really looking forward to switching over to v3 when its out. |
I've begun work on version
3.0
which will be a mild to moderate rewrite of some of variant's core.Changes now accessible with
variant@dev
Changes
Name swap.
Right now the
variant()
function is used to construct a single case, whilevariantModule()
andvariantList
are used to construct a variant from an object or a list, respectively. This leads to the function that I almost never use (single case of a variant) holding prime real estate. To that end,variant()
is becomingvariation()
and thevariant()
function will be an overload that functions as bothvariantModule
andvariantList
. This is a notable breaking change, hence the tick to 3.0.Less dependence on
"type"
The
type
field is used as the primary discriminant. This is causing an implicit divide in the library and its functionality as I tend to write things to initially account for "type" and then generalize. However, this is not ideal. My plan is to take the approach withvariant
actually beingvariantFactory('type')
.The result will be an entire package of variant functions specifically working with
tag
or__typename
.This should handily resolve #12 .
Better documentation and error handling (UX as a whole)
I've received feedback that larger variants can be tough to read when missing or extraneous cases are present. I will be using conditional types and overloads to better express more specific errors.
I will also be rewriting the mapped types so that documentation added to the template object of
variantModule
will be present on the final constructors.Assorted cleanup
This will officially get rid of
partialMatch
now that it's no longer necessary. I'm not sure, but lookup may go as well. It's on the edge of being useful. I personally almost never use it. I've thought about allowing match to accept single values as entries but am worried about ambiguity in the case where a match handler attempts to "return" a function. How is that different from a handler branch, which should be processed.Also I will probably be removing the default export of this package (the variant function). Creating a variant, especially with the associated type, involves several imports by itself. What would be the point of a default export? Perhaps
variantCosmos
, but even that is a bit sketchy.I finagled with the
match
types and now the assumed case is thatmatch()
will be exhaustive. There is an optional property calleddefault
, but actually attempting to use said property moves to the second overload, flipping expectations and making default required and every other property optional. This results in a better UX because as an optional propertydefault
is at the bottom of the list when entering names for the exhaustive match.The text was updated successfully, but these errors were encountered: