-
Notifications
You must be signed in to change notification settings - Fork 618
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
Add super basic generics - project static #376
Conversation
* @param {*} value The value to unbind from the key. | ||
* @param {number} priority The priority of the binding. | ||
*/ | ||
unbind(key, value, priority) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it possible to have multiple values with the same priority?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is possible to have:
- Multiple different values with different priorities.
- Multiple different values with the same priority.
- Multiple of the same value with different priority.
- Multiple of the same value with the same priority.
So basically: Yes. The PriorityQueueMap doesn't really have any fancy behavior atm, it's just a wrapper for a map where all of the values are priority queues.
Okay, I'm diving back into this and will have a review for you today. Sorry about the delay! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This makes sense overall, though it feels like the interactions are extremely complicated.
With respect to the deep generic nesting, I do wonder if you instead should make it so that you can't do Identity -> Identity -> Identity -> Dog. What if you blocked nested identity blocks entirely, so something has to be bound more immediately? (I have not thought all the way through this for all of the different use cases).
You may want to export a MIN_PRIORITY and MAX_PRIORITY for the generic map.
if (e.newParentId) { | ||
parentBlock = this.workspace_.getBlockById(e.newParentId); | ||
parentCon = parentBlock.getInput(e.newInputName).connection; | ||
explicitFn = GenericMap.prototype.bindTypeToExplicit.bind(genericMap); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are you using a bind instead of just calling the function, given that you also have the object around?
If the goal is only to make it possible to directly call the correct function, I think it would be clearer to set a boolean saying which case it is and then have a helper that checks that boolean and calls the appropriate function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah this is a really tricky spot, and I still have mixed feelings about it.
The reason I originally aliased these functions instead of just calling them is because I wanted to try and keep the logic at the end of the function as simple as possible. All it cares about is the state of the connections (E, GU, or GB) so I wanted to move the other info about binding vs unbinding somewhere else.
Adding checks for binding vs unbinding at the end of the function would definitely work, but my current feeling is that it would distract from the logic that's really important. I know it's annoying to refer back to the actual function that's getting called, but the idea is that you shouldn't have to do that often because again, that info isn't really important. What's important is when it calls the generic function vs when it calls the explicit function.
That's just my current feeling on it though. As this function expands (especially as I add logic for flowing bindings through connections) something else may be better.
if (!priorityMap) { | ||
return undefined; | ||
} | ||
const types = priorityMap.getValues(genericType); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is types guaranteed to have at least one entry if it's not null?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I checked, and the PriorityQueueMap handles that here. I'm not sure if I handled it correctly for the dependersMap_ though so I'll have to check. And I'll add some tests as well!
I was thinking that we you could make it impossible to chain identity checks regardless of the order. I suggest 0 for a min priority. Int max for the max is fine. As far as the complicated code goes--not sure, it's possible it'll be easier to understand when more of the pieces are in place. |
Approved--go ahead and merge when you're ready. |
@moniika It looks like I might have messed something up with the field date tests... The results from travis look weird so here's what I get locally as well: Console outputRan at 19:09:17 PT
I've got no clue what it is. |
I know what it is. It's on me. I made a mistake in the tests (with how I get the "expected" default value for the field. I'm going to make a PR to fix the test. |
I think my PR should fix the Travis failures. The issues you have locally (with the date being off by 1 day) I thought were already fixed. Let me know if after merging if it works locally. |
d2d7b39
to
5905ec3
Compare
Yep yep it works perfectly! I just forgot to rebase hehe. Thank you so much :D |
Preface
This is a bit of a weird PR because it's mostly puzzle pieces that will hopefully come together later. But I'll do my best to explain where my brain is going.
Also, I know this PR description is super long, and I still don't think I did a good job explaining. So if there's anything that's unclear or anything I can do to make it more clear please tell me.
Basic Idea
Let's start with a simple example of generics. Below there is a generic Identity block with a connection check T on its input and output. After we connect the Dog block to the Identity block we want the T check to start acting like a Dog check.
Now say we connect annother Identity block to the first Identity block. We now want that block's T check to /also/ start acting like a Dog check.
There are two ways we could implement this.
Whenever we connect a connection with a generic check to another connection, we create a "pointer" to the connection check of the other connection. Then when we go to get the explicit type of the generic check, we do a recursive lookup to see if it is bound to an explicit type anywhere.
So if we were to connect these two Identity blocks their T types would be bound to eachother.
We only "bind" a generic check to another check if the other check is an explicit type, or has an explicit type associated with it.
So if were were to connect these two Identity blocks their T types would /not/ be bound to eachother.
But if we added a Dog block to the second one, then our bindings would update to look like this:
I decide to go with option 2. The logic will be more complicated, but I believe it will be more efficient.
Efficiency
For example, say we implement option 1 and then I build the following set of blocks:
Each time I connect an Identity block, the growing blob of blocks would need to recursively check every connected block to see if it is bound to an explicit anywhere.
Whereas option 2 wouldn't require any of that, because none of the blocks are bound yet. It would only require a recursive update if/when I were to eventually connect an explicit type.
Complicated logic
To implement option 2 we need to do different things after two connections connect, depending on the "kinds" of the connections.
So currently a connection check can have three different "kinds"
A) It can be explicit, eg Dog - E
B) It can be generic, and not currently bound to an explicit type - GU
C) It can be generic, and bound to an explicit type eg Dog - GB
Note that we do want to bind already bound generics (GBs) to Es and other GBs. The reasoning for this is explained below in the PriorityQueueMap section.
This is what the logic here is meant to do.
We also need to do similar unbinding when the two blocks disconnect, except it's more complicated, because once an unbound connection is connected, it becomes bound.
EGUGUEGUGBGBGULuckily the unbinding logic pretty much works even though connections that were previously GUs are now GBs (since they've been bound by connecting). We just have to make sure that the GenericMap (see below) fails graceful if we try to delete a binding and the binding doesn't exist.
Current logic limitiations
Note that currently the logic doesn't handle updating the checks that are dependent on a block when a block updates.
So if you connect the explicit block first, and then continue connecting generic blocks all of the bindings "flow through" the connections.
But if you connect the generic blocks together first, and then connect the explicit block, that's not handled yet.
Puzzle Pieces
PriorityQueueMap
This is a new data structure I created that works sort of like a map, except that keys are mapped to priority queues of values. So a key can be bound to multiple values, but it's actual value(s) is the value with the highest priority.
This new data structure is used to bind generic type names to their explicit type names, within the context of a block. It will probably also be used for generic type constraints, once I get there.
The reason we cannot implement type binding using a simple map is twofold:
For example right now the generic type (T) of the Select Random block is unbound:
Now it should be bound to Dog:
Now Mammal:
And now Cat:
Allowing a type to have multiple bindings feels to me like the best way to handle any binding being removed at any time, and forcing the generic type to act like one of its other types.
Generic Map
The NominalConnectionChecker decides when to bind and unbind types, but the GenericMap handles the consequences of that decision (basically).
The generic map has two things it keeps track of.
(1) is useful for finding the explicit type of a generic type so that we can perform connection checks. (2) is useful for unbinding types. It will also allow us to have bindings “flow” through dependent types, once I add that.
Problems related to blockly APIs
So currently all of the binding is handled through event listeners, which causes two problems:
Adding a way for a connection checker to get a callback immediately after connect & disconnect would solve both problems, but that's really only useful if you want to implement generics.
If we decide to stick with the events system I'll probably end up adding a Promises system to fix (1) and hooking into the FinishedLoading event for (2)