Skip to content
This repository has been archived by the owner on Aug 29, 2024. It is now read-only.

WIP: Substitute symbols for expressions from optional scope #212

Open
wants to merge 13 commits into
base: master
Choose a base branch
from

Conversation

Ephs05msm
Copy link

This is some work based on a discussion in #209. This happens to be my first ever pull request, so please provide any feedback on things that seem out of sorts. I referenced the contributing guidelines and then followed the instructions from this video to get to this point.

With that said, the approach I ended up taking toward this issue was to:

  • Add an optional scope argument to solveEquationString and simplifyExpressionString
  • Add a module, Substitute.js, that iterates over a provided scope and replaces symbols with their respective expressions. The intent is to account for nested symbols and the unit test I added to solveEquation.test.js uses such a scenario. I placed the module in ./lib but it likely belongs somewhere else.
  • Add a ChangeType, SUBSTITUTE_SCOPE_SYMBOL, that captures instances where symbols are being substituted. I felt this was important to keep the user experience consistent with what it currently is.
  • Add a tree search type, substituteScopeSearch, which compares SymbolNodes against the scope. When a match is found, simplifyExpressionString is run (awkwardly due to a TypeError: "...not a function" issue I ran into -- please assist) against the substituted expression and the resulting list of steps (if any) are passed to Node.Status.nodeChanged.

All tests pass, although I only added one additional test to begin to cover this functionality.

I did my best to employ similar patterns to those I found in the codebase as much as I could. There are sure to be some clumsily-handled areas; please provide feedback on those as I am still learning.

Thanks and I look forward to your review,
Matt Marino

@Ephs05msm
Copy link
Author

Among other mistakes, I accidentally created a test file early on and then never ended up using it. Should probably be removed. Sorry about that.

@evykassirer
Copy link
Contributor

yeah feel free to go through and remove anything not directly related to your PR

I noticed there's also a yarn.lock file which shouldn't be in this PR either - you could add it to the gitignore file!

I think it's getting late for me tonight but I hope to take a look at this in the next couple days :) Thanks for your hard work (it's so cool seeing your learning process and how you do so much without much formal educational background!!!) and for your patience :)

@Ephs05msm
Copy link
Author

Sounds good. Couple more commits to get rid of some of the obviously unnecessary stuff. Still a bit of a noob with .gitignore so struggled to get the .vscode folder out. Sorry about that.

Saw a conflicting note regarding yarn.lock so just left it alone for now.

Look forward to your feedback whenever you have time. No rush at all on my end. Thanks again!

@Ephs05msm
Copy link
Author

Ephs05msm commented Jul 27, 2017

Not directly related to this issue, but I wanted to ask a quick question:

While using this code on another project I came across the following scenario that confused me. I added it to the test suite on my local branch and got the same result:

capture2
capture

Same result on master as well (using ['x = 100 + (25^2 / 4)', 'x = 256.25']).

Is it by design that MathSteps won't simplify fractions that could be expressed as a float but not an (unrounded) integer?

@evykassirer
Copy link
Contributor

answered in #215 :)

@Ephs05msm
Copy link
Author

Ran into a case where a symbol was nested inside of a UnaryMinus node type and adjusted to account for this. Also added a test to cover. My editor (VS Code) was having end of line sequence issues so fixing those makes it look like I changed a lot of things when in fact only the tests, TreeSearch, and scopeSubstitution search type were meaningfully changed. Sorry about the extra noise. All precommit checks pass.

Copy link
Contributor

@evykassirer evykassirer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi! Thanks for your patience in waiting for review :)

This looks super awesome! I am very impressed that, with no formal background in CS, you were able to just jump in and make this big change and follow the style of code already here and wow 🎉

I added a bunch of comments (I always do in pull requests hehe) mostly small things, but a few bigger changes. No opinions are absolute so let me know if you think something else would be a better idea or if you have questions about what I mean

Also a high level comment:

I think instead of passing in scope, we should pass in options.

right now it'd look like:

options = {
  scope: { ... }
}

Then later if we want to something else alongside scope, we don't have to add an extra argument everywhere, we can just add it to the dictionary. How do you feel about that?

@@ -96,6 +96,8 @@ describe('solveEquation for =', function () {
['2/(1 + 1 + 4x) = 1/3', 'x = 1'],
['(3 + x) / (x^2 + 3) = 1', 'x = [0, 1]'],
['6/x + 8/(2x) = 10', 'x = 1'],
// Use of nested scope (i.e., baz depends on bar)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you actually make a new test file just for testing with scope? :) and add a bunch more tests for more equations but also expressions (no equals sign)

I'd also reoreder the arguments in the new test file so that the scope comes after the input and before the output (makes a bit more sense when reading it)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ALSO THIS IS REALLY COOL that you added this functionality!!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oo also could you test substeps to show how things get sub'd in one at a time? there are some tests that already test substeps you can probably base off of but let me know if you want help setting that up

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made some progress on this, but I struggled to get any of the simplify tests to pass. Decided to push my incremental changes in hopes we can work through this together. The simplify tests weren't originally designed with scope in mind, and my quick attempt to incorporate it fell short.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The simplify tests weren't originally designed with scope in mind, and my quick attempt to incorporate it fell short.

Yeah I think putting the tests for this in their own completely different file would help - then we don't need to update the other tests to deal with scope and can do a different setup for these tests. Let me know if you want help going through that - I'm a lot more free this week and next than I have been recently and would love to help (sorry for just getting to this PR again now!)

.gitignore Outdated
node_modules
*.log
.DS_Store
.idea
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you keep the new line at the end of the file? Github seems to like it and puts a 🚫 if you don't, so I've adopted liking it too ^_^

I see it in a few other files, so just scroll this PR and you'll see where!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hopefully caught all these in my latest PR. Will pay attention to this going forward.

if (status.hasChanged()) {
return Node.Status.childChanged(node, status);
}
}
// Break out isSymbol test and add a changeType for SUBSTITUTE_SYMBOL?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't need this comment (it's more like a commit message and less like a long term comment, like above)

Though if you want to keep a comment here it'd probably be more like // symbols can be simplified through variable substitution

return Node.Status.noChange(node);
}
// Moved isUnaryMinus check above Symbol check to address symbol nested inside unaryminus
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

try to avoid saying things like "moved" since future people looking at this code will only know the current state, not the past and that'd be confusing!

you could instead say something like "isUnaryMinus needs to be checked before Symbol because ..."

Though I'm confused why it has to be first. What test would fail if you checked symbol first? A unary minus is not a symbol, and unary minus recurses on its child! (let me know if I'm being confusing, not sure if I'm explaining this well ^^)

@@ -0,0 +1,16 @@
function substituteScope(scope) {
Copy link
Contributor

@evykassirer evykassirer Aug 6, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you name this file substituteScope.js to match its name? :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also can you add a comment explaining the purpose of this function? I'm a bit confused about what it does - is it replacing scope things? Shouldn't that happen in the steps instead?

@@ -69,6 +70,9 @@ function step(node) {
const simplificationTreeSearches = [
// Basic simplifications that we always try first e.g. (...)^0 => 1
basicsSearch,
// Substitute symbols present in the optional scope object with
// their respective expressions.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you add an example similar to the other simplification searches here?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added an example but let me know if it needs to be reworded.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks good to me! awesome
I think you can take out the comma after the e.g. though


// Searches through the tree, prioritizing deeper nodes, and substitutes
// in-scope values for their respective expressions on a symbol node if possible.
// Returns a Node.Status object.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

awesome documentation :D

}

let symbolName;
if (node.type === 'SymbolNode') {
Copy link
Contributor

@evykassirer evykassirer Aug 6, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can use Node.Type.isSymbol(node) instead?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WAIT does this return true for unary minus too?

okay this makes sense https://github.com/socraticorg/mathsteps/blob/344d15b5d5e25251cef7d6976d6a73df16661d7d/lib/node/Type.js#L32

For any isSymbol thing you've added in this PR can you pass in false for the allowUnaryMinus argument? Then it can naturally recurse instead of you having to deal with it here.

Sorry, that's weird that we did that (though I'm assuming there's a reason I've now forgotten XD but feel free to investigate if you're curious or open an issue)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left this alone for now since you followed up with the change to allowUnaryMinus. Let me know if we should revisit.

}

if (scope.hasOwnProperty(symbolName)) {
// when declared at top, kept getting TypeError: ___ is not a function
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you explain a bit more about when the error happens? I can try replicating it on my computer and see what's up. Did it say ___ is not a function or did it give a specific function name there?

if (scope.hasOwnProperty(symbolName)) {
// when declared at top, kept getting TypeError: ___ is not a function
const simplifyExpression = require('../../simplifyExpression');
const substeps = simplifyExpression(scope[symbolName]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of simplifying the expression here, I think it's best to just do newNode = math.parse(scope[symbolName])

then any simplification of the value can happen in later steps instead of substeps and it makes this code less complicated!

what do you think?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit turned around here, perhaps due to the approach I took. Since simplifyExpression is where the scope substitution takes place, I'm concerned calling math.parse(scope[symbolName]) here won't be effective since it will not perform the scope substitution. That may be why you advised to put the substituteScope call deeper in the simplify function?

Left as is in my recent (incremental) PR.

@evykassirer
Copy link
Contributor

ok so I looked into it and there's no reason for allowUnaryMinus to default to true - I've updated that on master, feel free to pull that in and then your code should work as it was before :)

@Ephs05msm
Copy link
Author

Thanks so much for the detailed feedback! I'm all for your suggestions and will get to work on them this week. If I get in over my head anywhere I'll ask for help.

Also, I really appreciate the encouragement. I've found there's a lot of self-doubt along the learning path (at least doing it the way I have been), so even a little bit of affirmation that some things are being done correctly is hugely helpful. Thank you.

On we go :-)

@Ephs05msm
Copy link
Author

I replaced the scope arg with options as you suggested, and also attempted to reorder the args so debug is last. Tests pass but it's certainly possible I didn't address it in every place/case. Happy to double back with you if you find issues.

@Ephs05msm
Copy link
Author

Thanks again for taking the time to review. I think this latest round of changes get closer to the mark, but there are still some loose ends (especially on the testing side). I felt it better to commit some progress than hold back just to spin my wheels in a few spots. Look forward to continuing to work with you on this :-).

@@ -0,0 +1,24 @@
function substituteScope(scope) {
Copy link
Contributor

@evykassirer evykassirer Aug 30, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just copying from before so it's easier to keep track of what's left (no worries for not getting to it yet)

also can you add a comment explaining the purpose of this function? I'm a bit confused about what it does - is it replacing scope things? Shouldn't that happen in the steps instead?

if (scope.hasOwnProperty(symbolName)) {
// when declared at top, kept getting TypeError: ___ is not a function
// when declared at top, kept getting
// TypeError: <nameIGaveToFunction> is not a function
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

^ still gotta figure this thing out

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will look into this...

return Node.Status.noChange(node);
}
// Check isUnaryMinus before Symbol because symbols nested inside unaryMinus
// (e.g., 2x - symbol) need to be subtracted from the expression. Checking
// symbol first causes the symbol to be added.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think with my unary minus change, it doesn't have to be checked first - can you switch it back and see if that works? Before, symbols with unary minuses were counted as symbols too by default, but now it Node.Type.isSymbol(node) on a unary minus node should return false by default :D

if (node.type === 'SymbolNode') {
symbolName = node.name;
}
else if (Node.Type.isUnaryMinus(node)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

now that I updated the Node.Type.isSymbol definition, you'll never get unary minuses in here anymore which makes things much easier hopefully!

so you can take out if (node.type === 'SymbolNode') { and else if (Node.Type.isUnaryMinus(node)) { and always just do symbolName = node.name;

const scope = options.scope;
if (scope.hasOwnProperty(symbolName)) {
// when declared at top, kept getting
// TypeError: <nameIGaveToFunction> is not a function
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is nameIGaveToFunction - which function?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

...this was my way of trying to communicate that, no matter what name I gave to the function that made this call, it would fail with that error message. nameIGaveToFunction would be substituted with whatever I tried. IIRC, I pointed this out because I thought the issue might have been with a name conflict of some kind. Doesn't appear to be the case, though.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

which is the function making the call? is it in the PR here? what's it called right now?

There's so many functions in the code haha so confused which one you're referring to!

if (scope.hasOwnProperty(symbolName)) {
// when declared at top, kept getting
// TypeError: <nameIGaveToFunction> is not a function
const simplifyExpression = require('../../simplifyExpression');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so simplifyExpression is a pretty high level function - it's the big thing at the top that calls everything else. Because of that I feel weird about using it here.

What you have here also means if you have x = 2/3 + 4/6, the substitution will go from x to 8/6 with the adding of fractions as substeps, which from a teaching perspective also feels weird to me.

My suggestion instead is:

  1. take out the const newScope = Substitute(options.scope); above and keep the options exactly as they were passed in
  2. here, instead of doing const substeps = simplifyExpression(scope[symbolName]); do newNode = math.parse(scope[symbolName])

I know it's a bit different from your idea, so we could chat more about this and make sure we're on the same page and both happy with whatever we decide to do. I think adding some tests that show multiple steps will also make it more clear what this changes. Let me know if you have more questions or comments about this!

@evykassirer
Copy link
Contributor

ok I think I addressed all your remaining comments/questions!

looks really awesome so far, I'm a fan of the options thing and debug last is 👌 (I am also not sure if you caught them all but seems good!) Thanks for your patience with my review and for working on this new really interesting feature!!

@Ephs05msm
Copy link
Author

Thanks, @evykassirer! Planning to spend time on this next week.

@Ephs05msm
Copy link
Author

Hi @evykassirer, just wanted to to circle back and let you know that I haven't lost sight of this. Just been focused on a few other things instead. Since this is an isolated branch representing a marginal feature, is it safe to assume that it's alright if I take a break for a bit? If not, I'll make the time.

Thanks again for your help.

@evykassirer
Copy link
Contributor

no worries! I think you're fine, but if the porting change goes through soon and messes you up I'd be happy to help you refactor your stuff (it'd be just a bunch of really small changes) :)

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

Successfully merging this pull request may close these issues.

2 participants