|
NetKernel News Volume 3 Issue 39
September 29th 2012
What's new this week?
- Repository Updates
- Durable Scope Blog - Dynamic Grammars
- Resource Oriented Analysis and Design - Part 6
- Preventing replay of taken cells
- Do we have three in a row? The CheckSet Meta Resource
- Linked Data Logic
- Set Membership Equality
Catch up on last week's news here
Repository Updates
The following updates are available in the NKEE and NKSE repositories...
- kernel 1.30.1
- Changed state transition so that hit of resolution cache now uses resolved scope to try for hit in representation cache further increasing opportunity for cache hit.
- layer0 1.92.1
- Fixed modules.d hashcode check so that first boot doesn't cause an unnecessary false synch.
The following update is available in the NKEE repository...
- cache-ee 1.3.1
- added constraint of 8 to number of cache keys that can be attached to one item to prevent growth of unnecessary unresolved scope keys.
Durable Scope Blog - Dynamic Grammars
This week Tony describes the many ways in which you can enable dynamically generated grammars and shows an implementation using NKF's ConfiguredEndpoint base class. Everything is a resource - even grammars...
http://durablescope.blogspot.co.uk/2012/09/dynamic-grammars.html
Resource Oriented Analysis and Design - Part 6
Sixth in a series of articles taking a well understood domain problem and casting it as an ROC solution. Read Part 1 Here, Read Part 2 Here, Read Part 3 Here, Part 4 Here, Part 5 Here
Recap
Last week we discussed how with ROC its a useful working practice to work from both edges towards the middle. We built a web rendering layer and added the ability to make a move. By delegating state management to the edges (client-side AJAX) we implemented a first rough cut of the game.
This week we're going to add a little finesse and in the process look at stateless business logic...
Preventing replay of taken cells
Last time, we left the game in a playable condition but noted that it was violating one of the basic rules of TicTacToe in that it was possible to overwrite the state of a cell by playing another move on it. So now we need to fix this.
All that we need to do is change the implementation of the res:/demo/tictactoe/move endpoint so that before SINKing the move's token state to the cell, it first SOURCEs the cell and checks that it is empty (ie the default platonic empty string "" cell state)...
cell=context.source("httpRequest:/param/cell") state=context.source(cell) result=null if(state.equals("")) { state=context.source("httpRequest:/param/state") context.sink(cell,state) result="OK" } else { result="TAKEN" } resp=context.createResponseFrom(result) resp.setExpiry(resp.EXPIRY_ALWAYS)
The resulting representation is now either "OK" or "TAKEN". In the HTML rendering layer, you'll recall that the AJAX script only changes the client side state if "OK" is received. So a response of "TAKEN" is ignored and therefore makes a repeat move on a cell impossible.
Do we have three in a row? The CheckSet Meta Resource
To be a slightly more sophisticated solution it would be nice if we can determine if someone has won. You'll recall that back in Part 1 we anticipated this requirement in our modelling and discussed Meta Resources. In particular I proposed the idea of the "CheckSet" resource.
The idea for this resource is that for any given cell, there must be a corresponding row, column and potentially diagonal(s) that we need to look at to decide if the game has been won.
So, say we play cell c:0:0, then we have to check the cells in row:0, column:0 and diagonal:0 to see if any of these sets is all equal. If one is, then the game has been won.
With a code-based perspective you might start by modifying the implementation of the move service to contain the logic to source all the necessary testable cells... but that would make for a very complicated lump of spaghetti business logic. Not something you'd want to write or maintain.
The ROC way is to think of the CheckSet as a resource. There is one CheckSet for every cell on the board. You'll see what I mean if I throw you in to the deep end of my implementation. In 30 seconds I added the following endpoint in the wwwMapperConfig.xml...
<grammar>
<active>
<identifier>active:checkSet</identifier>
<argument name="cell" />
</active>
</grammar>
<request>
<identifier>active:groovy</identifier>
<argument name="operator">
<literal type="string"> import org.netkernel.layer0.representation.impl.* cell=context.getThisRequest().getArgumentValue("cell") s=cell.split(":") x=Integer.parseInt(s[1]) y=Integer.parseInt(s[2]) b=new HDSBuilder() b.pushNode("test") b.addNode("id", "row:"+y) b.addNode("id", "column:"+x) if(x==y) { b.addNode("id", "diagonal:0") } if(y==(-1*x)+2) { b.addNode("id", "diagonal:1") } resp=context.createResponseFrom(b.getRoot()) </literal>
</argument>
<argument name="cell">arg:cell</argument>
</request>
</endpoint>
Briefly, I've used an active grammar that expects an argument named "cell". This will be the "c:x:y" cell identity for which we require the CheckSet.
In the first 4 lines of code I extract the identity (value) of the cell argument from the request. I know the structure so I split the c:x:y string up and parse the x and y integer values. To recall the methodology of last week: Step 1 is complete. I know what I've been asked for.
There is no step 2, since I don't need any other resource to satisfy this request. In ROC terms we can think of the CheckSet as being a Platonic Resource - it is an abstract entity like a "Platonic Form", it has always been there and it will always be there (no really).
Step 3 is the value add part. You can see that this demands that I get off my philosophical high-horse and get down and dirty with a concrete representation. I don't waste any time worrying about the physical form of the representation, as I have explained previously, a good rule of thumb is: when you don't know or don't care then use a tree-structure.
Here, using an HDSBuilder, I construct an HDS tree and push a root node of "test". Next I add an "id" node with a value of "row:y" (the identity of the y row) and also the same for "column:x" since every cell is always a member of one row and one column.
Dealing with the diagonals requires a little consideration of linear equations. We understand that the diagonals are lines and therefore must follow the function y=mx+c. In our naming convention defined in part 1 we agreed that diagonal:0 goes from top-left to bottom-right so this is the line y=x. Equally diagonal:1 goes from bottom-left to top-right. With a moments thought its apparent this is the line y=2-x.
Now, we only need to consider a diagonal if our cell is actually on either of the two diagonal lines. So now you see what the two conditions in my code are doing to take care of this.
To see what this endpoint provides, here are three examples of the CheckSet corresponding to cells top-left (c:0:0), top-center (c:1:0) and center-center (c:1:1). As we have repeatedly seen in earlier parts, these follow the pattern of being linked data resources containing references to the identities of other resources...
Cell | CheckSet |
c:0:0 |
<test>
<id>row:0</id> <id>column:0</id> <id>diagonal:0</id> </test> |
c:1:0 |
<test>
<id>row:0</id> <id>column:1</id> </test> |
c:1:1 |
<test>
<id>row:1</id> <id>column:1</id> <id>diagonal:0</id> <id>diagonal:1</id> </test> |
Lastly, returning to the implementation code, notice that I have not expired the response. In our constrained game board there are only 9-possible CheckSets. When we have generated one we can keep it forever. It follows that we should expect this code to run precisely nine times in the entire life of this system.
Now we need to see how to use a CheckSet...
Linked Data Logic
For each move made, the CheckSet tells us which resources need to be looked at to decide if the game has been won. So now I can enhance my move service...
cell=context.source("httpRequest:/param/cell") state=context.source(cell) result=null if(state.equals("")) { state=context.source("httpRequest:/param/state") context.sink(cell,state) req=context.createRequest("active:checkSet") req.addArgument("cell", cell) rep=context.issueRequest(req) ids=rep.getValues("/test/id") for(id in ids) { //Need to check the cells are equal println(id) } result="OK" } else { result="TAKEN" } resp=context.createResponseFrom(result) resp.setExpiry(resp.EXPIRY_ALWAYS)
After we SINK the cell state, we request active:checkSet for the cell.
The CheckSet's HDS tree structure allows me to select the set of id nodes from the tree with an XPath "/test/id". So now I know the identity of every resource I need to look at to see if the game is over. For the moment I just iterate over them spitting out the identity of each resource.
So now we need to know when a composite resource (a set of cells) is all equal...
Set Membership Equality
I copy and paste my active:checkSet endpoint declaration. I delete the code and modify the grammar to active:testCellEquality and specify an argument name of "id", the identity of the composite resource set of cells to test for equality.
Here's the implementation...
<grammar>
<active>
<identifier>active:testCellEquality</identifier>
<argument name="id" />
</active>
</grammar>
<request>
<identifier>active:groovy</identifier>
<argument name="operator">
<literal type="string"> rep=context.source("arg:id") cells=rep.getValues("/cells/cell") result=true; last=cells[0]; for(cell in cells) { if(cell.equals(last)) { last=cell; } else { result=false; break; } } resp=context.createResponseFrom(result) </literal>
</argument>
<argument name="id">arg:id</argument>
</request>
</endpoint>
Using our ROC methodology again, we can analyse this endpoint.
- Step 1 (what am I promising) is explicitly declared in the grammar. I have promised that I will provide the resource that says if another resource's cells are all equal.
- Step 2 (what do I need to do this). I obviously I need the resource in question. So I must source the resource specified in the "id" argument. Notice that NKF provides a convenient convention to do this. I can SOURCE "arg:id" since the "arg:" prefix says SOURCE the resource who's identifier is the value of the argument. If the id argument is "row:0" this will SOURCE row:0. Using "arg:" is like dereferencing a pointer.
- Step 3 (add value). We know that all composite resources in the TicTacToe resource model are implemented using HDS and have the tree structure /cells/cell. So using an HDS XPath we can get the values of all the cells. The logic looks at the cells, and allowing for fast fail on a difference, tests to see if all the cells have the same value.
- Step 4 (keep your promise). We return a boolean true if the cells are all equal.
Notice this code has no error handling. Why? Because if you pass it a resource reference that does not reify to an HDS representation with the correct structure it will throw an exception. We're saying "You're not keeping your side of the bargain and we're not the right resource for you to request - so deal with it buddy". But from the requesting side we also know that the resource identifiers that we will pass here are only ever going to be those in the CheckSet and we know from our initial construction of the TicTacToe resource model that those identifiers always reify a suitable representation.
We're now ready to finish the solution...
Final Step
So now, back in the move implementation, we can check each item in the CheckSet...
cell=context.source("httpRequest:/param/cell") state=context.source(cell) result=null if(state.equals("")) { state=context.source("httpRequest:/param/state") context.sink(cell,state) req=context.createRequest("active:checkSet") req.addArgument("cell", cell) rep=context.issueRequest(req) ids=rep.getValues("/test/id") result="OK" for(id in ids) { req=context.createRequest("active:testCellEquality") req.addArgument("id", id) if(context.issueRequest(req)) { result="GAMEOVER" break; } } } else { result="TAKEN" } resp=context.createResponseFrom(result) resp.setExpiry(resp.EXPIRY_ALWAYS)
For each id we now request active:testCellEquality for that resource. (Remember the CheckSet table above, for cell c:0:0 we request tests for row:0, column:0 and diagonal:0). As soon as we find that a resource contains all equal cell state then the game is over. The current move is the winning move and we have a filled row, column or diagonal.
Finally, back in the HTML rendering layer, we hack the AJAX code to deal with the new possibility of a "GAMEOVER" response...
function bindCell() { //alert("Binding"); $(".cell").click( function() { if(player!="GAMEOVER") { var cell=$(this); $.get("move?cell="+cell.attr("id")+"&state="+player, function(data) { if(data=="OK") { togglePlayer(); refreshBoard(); } if(data=="GAMEOVER") { refreshBoard(); alert("We have a winner! \n"+player+" wins") player=data; } } , "text" ); } } ); }
The game now plays for real and terminates naturally if a winner is found or the cells are all full.
Notes on working practice
This week I mostly refreshed and clicked cell c:0:0 over and over. But AJAX is a pain to develop if you don't know a powerful trick. For the entire development and evolution of the move service I kept the visualizer on. After each change I would clear the visualizer state, click cell c:0:0 and then dive in and look at the ROC domain requests.
I would do stupid things like have typos in my groovy implementation of CheckSet etc etc. The visualizer shows these exceptions and I could inspect them retrospectively at my leisure (by clicking "show response" on the red exception state).
By simple iterative refinement over the course of about 15 minutes I evolved my solution until it worked.
Next time we'll apply some final polish and add some constraints. In the mean time why not try the demo and use the visualizer to look at the requests and the cache hits. You'll find that our resource oriented solution is minimising computational operations and has normalized the solution...
Checkpoint
You can download a snapshot of the latest modules at this point here...
Note: You will need to accept last week's update to pds-core from apposite if you don't already have it.
Have a great weekend.
Comments
Please feel free to comment on the NetKernel Forum
Follow on Twitter:
@pjr1060 for day-to-day NK/ROC updates
@netkernel for announcements
@tab1060 for the hard-core stuff
To subscribe for news and alerts
Join the NetKernel Portal to get news, announcements and extra features.