(September 2015)
If you are are a developer, read on to see how I ported some old code from JavaScript and Canvas (i.e. dynamically-typed, imperative drawing) to TypeScript and React/JSX (statically-typed, functional style, state-driven - with compile-time checks of HTML syntax). The code can be downloaded from here.
And developer or not, you have to play Score4 below... and try to beat my AI :-)
Four years ago I implemented the AI Minimax algorithm and used it to play a game called Score4 / ConnectFour.
I did this after discovering the difference of doing things in functional (as opposed to imperative) style, coding it in many languages, and finally porting it to Javascript. The game was now accessible to any machine with a decent browser. Phones, tablets, desktops and laptops could all play it - One Platform to Rule Them All :-)
Years passed...
And as they did, the programming world - as usual - re-discovered some old truths...
...which I wanted to put to the test.
Equally important - this "magic function" can actually do a significant optimization: it can see which parts of your UI are associated with which parts of your state, and only update the displayed parts that were actually impacted by your state changes.
Clearer code **and** faster execution - could it be true?
I love Python ; and appreciate Javascript
and Clojure the Powerful, and generally speaking, the clarity
and brevity that usually accompanies dynamically typed languages.
You don't prefix each and every one of your variables
with type specifications, so only the "core" of your logic
is left behind. Go the extra mile and avoid loops, using
immutable map
s and filter
s and
fold
s, and you're all set :‑)
But let's be honest about something.
The lack of these type specifications means... that the errors that were traditionally caught by compilers... now blow up in your face.
At run-time. With your clients yelling at your support people.
And when you need to refactor... you must be really careful. As in, you have to create big test suites, and make sure they exercise large parts of your code (also known as "be religious about your test suite's code coverage"...). If you don't, code refactoring becomes an interesting exercise.
In hindsight, I think we gave up on compile-time checks too easily... what we really "longed for" was in fact cleaner, more expressive syntax (OK - that, and the rapid edit-run cycles). Personally, I've immersed my backend work in PyLint and Pyflakes and PyCharm's inspector... but all those tools still don't compare with what a language with a strong type system provides.
And as fate would have it, in the frontend, TypeScript - an open-source statically typed language that builds on top of Javascript, recently acquired support for a big part of React: JSX syntax.
That was all the incentive I needed - I wanted to taste React for quite some time, so I decided to put these two technologies to the test.
To make the test more representative of what a real application would do, I also wanted to try to make my UI as declarative as possible - which means that instead of using imperative Canvas drawing commands, I wanted to switch the code to CSS-based rendering of the game's board.
I started with the easy part - removing all the Canvas drawing code, and
all the related artifacts... like mouse-click-to-column-mapping code.
This would now be handled by onClick
DOM handlers.
I then had to figure out how to create a board using only HTML/CSS. I am not a frontend engineer, so... this required a bit of digging. Eventually I settled on a simple HTML table, with CSS-generated circles as tiles (inside the table cells).
.no_coin { width: 50px; height: 50px; } .red_coin { border-radius: 50%; width: 50px; height: 50px; display: inline-block; background: red; }
red-coin
then it will show up
in the board's table as a red game tile:
<table class="grid_table"> <tr> <td><div class="red_coin"></td>< ...
Next step: state... What is the state of my game - the one that influences my UI?
Well...
number[][]
string
number
s).
And that's it, really:
class Score4State { board: number[][]; info: string; wins: number; losses: number; }
Given an instance of this state, how do we render it with React to the table elements that compose our UI?
Turns out this part was easier than I thought.
We begin by telling React to render our Score4 component in our page,
at the place where we have a board
div:
showComponent() { React.render( <Score4 />, document.getElementById('board')); }
<div id="board" align="center"></div>
And our component will traverse the board, emitting the cells based on their state...
To ease the traversal, since poor Javascript lacks a range
, I provided
one:
var range = (n:number) => { var result = []; for(var i=0; i<n; i++) result.push(i); return result; };
...and used it to loop vertically and horizontally on my board, generating React elements as I went:
var proclaim = (n:number) => String(n) + " victor" + (n === 1? "y.":"ies.") ; return ( <div> <span style={{color:"green"}}> <b>You</b>:</span> {proclaim(this.state.wins)} <div style={{display:'inline-block'}}> <table className={"grid_table"}> { range(Score4_AI.height).map( y => ( <tr key={y}> { range(Score4_AI.width).map( x => cellMaker(y, x) ) } </tr> ) ) } </table> <p>{this.state.info}</p> <button type="button" onClick={resetAndRepaint}><b>New game</b></button> </div> <span style={{color:"red"}}> <b>CPU</b>:</span> {proclaim(this.state.losses)} </div> );
This looks very PHP-ish... until you realize that everything is checked
for correctness at compile-time. Try modifying any part of the HTML in that code, say
rename the style
attribute to styl
, and watch the
TypeScript compiler complain...
score4.tsx(148,23): error TS2339: Property 'styl' does not exist on type 'HTMLAttributes'.</pre>
And of course that doesn't just apply to your HTML "template" - try goofing on the
code itself, mis-typing yy
instead of y
, and...
score4.tsx(155,42): error TS2304: Cannot find name 'yy'.</pre>
Complete integration between the two worlds...
Wow.
And you are probably wondering about that cellMaker
; why didn't I just
emit the <td>
s in my inner "loop"?
Well, that was just by choice. In my humble opinion, separating them is cleaner. It also shows that generation of React components can be split up in whatever manner we choose:
var cellMaker = (y:number, x:number) => { return ( <td key={x} onClick={self.handleClick.bind(self, x)}> <div className={ (() => { switch(self.state.board[y][x]) { case 4: return "red_won_coin"; case 1: return "red_coin"; case -1: return "green_coin"; case -4: return "green_won_coin"; default: return "no_coin"; } })() } /> </td> ); };
Notice that JSX expects expressions in the JS bodies - and since switch
is
a statement in TypeScript, I had to enclose it in an anonymous function and
immediately call it.
Let me start by saying that the code that renders the board is now something like 40% of the corresponding Canvas code.
But that's just the cherry on top - what really matters, is that inside that
handleClick
member of my class, I just update the state...
var newBoard = this.dropDisk(this.state.board, column, -1); ... this.setState({ board: newBoard, info: msg, wins: newWins, losses: newLosses, });
That's all there is to it - React will automatically figure out what changed since last time, and will update the page to reflect the changes.
Wow. "Oh my God" kind of wow.
Well, frontend development has certainly changed a lot since the age of jQuery... Even though I only ported a simple game, with a UI layer of 200 lines of code, the difference in the mechanics used is very impressive. I can only imagine what React's impact will be in major UIs with complex state - and how many errors TypeScript will shield you from.
For example, to create the React component, I had to explicitely pass the
type of my state (Score4State
)...
class Score4 extends React.Component<any, Score4State> { brain: Score4_AI; ...
...and tsc
(the TypeScript compiler) guided me when I misused
my this.state.<fieldname>
accesses.
Overall, I am very pleased to see how all these technologies have evolved. And if I ever get back into frontend coding again, I will definitely make use of them.
Now go play Score4, above :-)
Index | CV | Updated: Sat Oct 8 11:41:25 2022 |