-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Learning QL: Add river crossing puzzle [SD-3133] #1985
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
Conversation
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 put one comment where I think the guidance could throw people down a difficult path.
I do think the structure is logical, albeit different from how I think about it. Personally, I see it as a search through a state space, leading to two challenges: Model the state and implement the search. However, the bottom-up thinking described here probably works much better for a tutorial.
Define a class ``Shore`` containing ``"Left"`` and ``"Right"``. | ||
|
||
It would be helpful to express "the other shore". You can do this by defining a member predicate | ||
``flip`` in the class ``Shore`` such that ``"Left".flip()`` returns ``"Right"`` and vice versa. |
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 might even work, and read nicer, to call the predicate "other".
action is "safe", that is, it doesn't lead to a state where the goat or the cabbage get eaten. | ||
For example, follow these steps: | ||
|
||
#. Define a predicate ``eats`` that encodes the conditions for when something gets eaten. |
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 formulation does not make me think of a predicate with two arguments for predator and prey. Maybe something along the lines of "...for when a predator would be able to eat an unguarded prey" would provide a stronger hint. (Even though predator and prey also seem slightly wrong for the goat-cabbage situation.)
} | ||
|
||
from string path | ||
where any(InitialState is).reachesVia(path, _) = any(GoalState gs) |
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.
A nice feature of high-level code is that it often reads like prose. Perhaps change the variable is
to s
to avoid it reading like bad grammar.
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 haven't tried this puzzle but have just reviewed the text. A couple of minor points, but that's all from me.
It seems like a sensible approach from my point of view.
*Show/hide code* | ||
|
||
.. literalinclude:: river-crossing.ql | ||
:lines: 15-22 |
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 is cool. I don't think that I'd realized that we could include just a section of a file. 👍
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 think this should be :lines: 15-23
|
||
/** | ||
* Holds if predator and prey are on the same shore and the man | ||
* is on the other shore. |
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'd suggest changing this to "and the man is not present" - since this is also true if the man is in the ferry and not on either shore.
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.
The way it is being modelled here, the ferrying steps are instantaneous and states are computed "before" and "after" ferrying. Therefor the man is never in the ferry when looking at the states. However, mentally, I can see that you are considering the state that the man is leaving behind, and the wolf probably has no intention of waiting for the man to reach the other side before eating the goat...
Should the |
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 have a few comments, but overall I think this is really good.
The structure guides the reader through in a logical fashion and, as Rasmus says, this approach is probably best for a beginner tutorial.
} | ||
|
||
/** A record of where everything is. */ | ||
class State extends string { |
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 would prefer each variable declared here to be more obviously linked to a location. Using the cargo name by itself is a tiny bit confusing to me. Would manLocation
, goatLocation
etc be clearer? Perhaps that would make later parts of the query look more complicated than is necessary though.... 🤷♂
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.
Good point. I've changed them all to manShore
, goatShore
etc, which makes things clearer in most places. The only problem was the eats(predicateShore, preyShore)
predicate, which looked a bit funny. I've changed it to eating
, which possibly helps?
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 guess you want some synonym of eatingSituation
, but I can only think of silly ones... (eatery
, snackTime
, etc..)
Thanks @jf205 - I've incorporated your suggestions locally (along with some other edits). |
I've finally tidied up the example queries in the last section and removed the TODO. @RasmusSemmle - could you check that the linked queries look okay? I've included one using @AlexTereshenkov - perhaps you'd like to have a look too? (This is also an example of including code snippets in |
You might want to harmonise the denotations across the linked solution. For instance, the first link writes "ferry X to the far shore" and "ferry Y back" rather than "to the right" and "to the left". I the third link, I would define You might also want to rename I think it is fine to show the |
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.
@shati-patel asked me to have a quick look as another pair of eyes and also being roughly the target audience for this tutorial. I spotted a couple of tiny typos when including the code snippets, and also a couple of comments on the QL that you are free to take or ignore.
State reachesVia(string path, string visitedStates) { | ||
// Trivial case: a state is always reachable from itself. | ||
this = result and | ||
visitedStates = "" and |
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.
For the base case should it be visitedStates = this
? Otherwise I think it would consider the path A => B => A
to be valid and wouldn't detect the loop.
exists(string pathSoFar, string visitedStatesSoFar, Cargo cargo | | ||
result = this.reachesVia(pathSoFar, visitedStatesSoFar).safeFerry(cargo) and | ||
// The resulting state has not yet been visited. | ||
not exists(int i | i = visitedStatesSoFar.indexOf(result)) and |
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 think this could be written as not exists(visitedStatesSoFar.indexOf(result))
and omit the variable declaration, though I'll leave it up to you to decide if that's a improvement.
} | ||
|
||
from string path | ||
where any(InitialState i).reachesVia(path, _) = any(GoalState g) |
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 think it would be equivalent to write this as
any(InitialState i).reachesVia(path, _) instanceof GoalState
Again up to you if you think it's an improvement or not.
*Show/hide code* | ||
|
||
.. literalinclude:: river-crossing.ql | ||
:lines: 14-55,105-115 |
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 think this should be :lines: 14-55,106-116
*Show/hide code* | ||
|
||
.. literalinclude:: river-crossing.ql | ||
:lines: 15-22 |
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 think this should be :lines: 15-23
Thank you for having a look! I've made some changes (including what you mentioned offline about the Let me know what you think. It should hopefully be in a mergeable state now 🤞 |
Once you've defined all the necessary classes and predicates, write a `select clause <https://help.semmle.com/QL/ql-handbook/queries.html#select-clauses>`__ | ||
that returns the resulting path. | ||
|
||
.. container:: toggle |
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 think it would be good to add a sentence explaining the use of _
in the select clause (including a link to
https://help.semmle.com/QL/ql-handbook/expressions.html#don-t-care-expressions).
Thanks for all of the updates @shati-patel! I have made one comment that would be good to address before we merge this. Do you think that there should be a link to this topic at the end of the detective tutorials? (Perhaps in the 'What next?' section here https://help.semmle.com/QL/learn-ql/beginner/heir.html |
Thanks @jf205 - I've added a link from the final detective tutorial. It doesn't really fit on https://help.semmle.com/QL/learn-ql/beginner/ql-tutorials.html, but there's already a link from the global Learning QL page, so hopefully it's sufficiently visible! |
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.
Thanks @shati-patel!
This is an initial attempt at turning one of the "QL etudes" into a tutorial.
The fun of the puzzle should come from writing your own solution, so I've tried to explain a model solution, but also suggest that people experiment more freely... It still needs work, but it would be helpful to get some feedback first :)
@RasmusSemmle @jf205 @felicitymay - you may have suggestions? Here is a preview.
In particular: Does this kind of format work? Is the query itself easy to follow? Should the walkthrough provide more/less guidance? Does it follow a logical structure?