[Ur] Ur/Web first impressions
Joachim Breitner
mail at joachim-breitner.de
Wed Mar 17 18:36:28 EDT 2021
Hello Ur/Web community,
I recently got interested in Ur/Web again, and set out to play around
with it a bit. This message is a wild mix of early beginner feedback,
questions, experience report and other chatter. Nothing very deep,
nothing new, so feel free to ignore… I am not necessarily expecting a
response. But maybe someone finds it interesting and wants to comment?
## Onboarding experience and first impressions
I began by reading most of the the manual front-to-back, to get a good
overview of the moving parts. That went well. I installed ur/web via
nix-env, as Debian stable currently doesn’t ship Ur/Web, that also went
smooth, and I had my first hello world running quite quickly.
I had read some experience reports about Ur/Web before, and was
prepared for not great error messages. Indeed, “This is an expression
but not a pattern.” and “Invalid FFI mode” were mostly unhelpful :-). I
learned that often they are due to a trailing ; or sometimes the
misguided use of List.map. Expectations were managed, so this was fine.
I got “Some constructor unification variables are undetermined in
declaration” for unification variables in unused code (e.g. when I
stopped using some temporary recursive helper function). Made me wonder
if undetermined variables could be maybe be allowed in unused code, so
that I don’t have to comment out that code?
In terms of developer usability, I was kinda expecting a “urweb watch”
mode that, upon changes to the sources, recompiles, restarts the
server, and maybe reloads the client. But not a big deal, I am just
spoiled.
## The project that I wanted to port
I thought it might be a nice small project to re-implement a game that
I wrote in meteor.js some 7 years ago. It matches two players, relays
the moves, also allows local hot-seat playing, and uses Canvas to
render the UI.
It wasn’t tiny, but also not big:
Server: 100 loc
Game logic: 256 loc
HTML/CSS: 50 loc
Client side JS:5 500 loc
(You can play the JS version at http://sumserum.nomeata.de and see thecode at
https://github.com/nomeata/sumserum if you care)
## Porting success
I was not able to finish the porting in the time I hoped I could do it.
Partly because it just isn’t a trivial task (so rewriting it in a
language I’d already be more fluent in would have taken it’s time as
well), but there was also some friction with Ur/Web that I encountered.
On the server side, the old app didn’t need any persistent state (it
only associated state with open connections), but in Ur/Web I had to
use the database to communicate between the open connections. A bit
more work, and in itself mostly smooth sailing. The “serialized” type
constructor was a bit odd; I wonder why can’t I just use the type
directly and instead have to serialize/deserialize, but I kinda see the
point.
I first ran into a problem with `client` values no longer being valid
when I redirect the user to a different page after match making. This
was documented, of course. To deal with that, I wanted to make my
client a single-page application. The typing prevented me from having
ta <dyn> that evaluates to everything, including <body>, but on
hindsight that’s actually good: The onload can’t really be dynamic,
after all.
Ur/Web doesn’t support canvas drawing out of the box, but I found
https://github.com/fabriceleal/urweb-canvas/. Using a library like that
was simple enough. Unfortunately, it didn’t cover the full Canvas API
yet, and it seemed that adding the missing functions would require more
boiler plate than I wanted to write right now. The JS FFI seems to be a
little less seamless than I would have hoped (it seems you have to
touch the .urp, the .urs and the .js wrapper for each member of the
canvas context object you want to add).
So I thought, also to save time, I re-use my existing JS drawing
functions and just expose one function
`drawState : canvas_id -> game_state -> transaction unit`
That didn’t quite go as expected, as JS arrays (which would be natural
to use in the game_state to pass, say, the list of fields) are not
supported by the JS FFI? So had to break it down into functions for the
individual elements:
type canvas_id = id
type side = int
val drawBackground : id -> transaction unit
val drawPlayerBoxText : canvas_id -> side -> string -> transaction unit
val drawPlayerBoxSel : canvas_id -> side -> int -> bool -> option string -> transaction unit
val drawPlayerCount : canvas_id -> side -> int -> transaction unit
val drawField : canvas_id -> int -> int -> option side -> bool -> bool -> transaction unit
val drawLine : canvas_id -> side -> int -> int -> int -> int -> bool -> transaction unit
Refactoring the existing JS code into that form was a bit tedious as
well, and it makes me wonder whether hooking up existing JS libraries
might be similarly unfrictionless. But maybe the need to import JS
libraries is just rare enough.
I kinda wonder if a more featureful FFI would be possible, which allows
more complex JS “types” to be described via Ur/Web types, and
interacted with seamlessly.
Obviously, I need to connect the above function to a source/signal with
the game state. I found that this works, although I wonder if this is
an idiomatic way to do so:
<canvas id={canvas_id} align="center" width=500 height=400/>
<dyn signal={
ui <- signal ui_state;
return <xml><active code={
drawDisplayState canvas_id (displayState ui);
return <xml></xml>
}/></xml>
## Other minor things
I found it odd that I cannot have mutually recursive values of type
transaction (which is a function of sorts), and have to add unit
arguments. Probably a ML programmer finds that restriction natural,
with my Haskell background it was odd.
Similarly, at one point I tried to use `s : source xbody <- source …`
for the main views (because why introduce a data type when there is
only a single function from that data type?). But that ran afoul a
similar problem, as … may not refer to `s`. Again, I was probably
thinking too Haskelly.
No derivation of show, soo useful for debugging and prototyping?
Not even for eq?
I missed a less heavy variant `let`. Especially one that lines up
nicely with do notation (`let val x = y in e end` is a mouthful, and
adds indentation). I resorted to `x <- return y; e`, not sure how bad
that is.
No eq for tuples predie
Irrefutable patterns in `p <- e; e` are useful, and I missed them also
in `fn p => …`, especially when working with association lists.
The CSS parser didn’t like some property names I was using:
“Invalid string -webkit-hyphens passed to 'property'”
Similarly, and just like almost any statically typed view on HTML,
there are always tags and properties missing. In my case, tabIndex. No
big deal, just minor friction.
At some point I briefly used variants, but the syntax was a bit
verbose, and they didn’t add anything of value over data types at that
point. But later I think I could have done fun fancy generic stuff with
them if I had them. Still makes me wonder if we can’t simply have both
(e.g. variants everywhere). Is it that type inference suffers too
much?
## Musings
Urweb generates data base schemas to create databases. But it seems the
problem of migrating existing data from prior versions to the schema of
new versions does not get any built-in help in Ur/Web. Ideally somehow
Ur/Web allows me to specify (declaratively or by code) how to migrate
from old versions, and I get the end-to-end static typing support that
I came to Ur/Web for. But it’s obviously a hard problem, and discussed
before (e.g. at https://github.com/urweb/urweb/issues/65). Still, I
wonder how production users of Ur/Web deal with this?
It seems that Ur/Web projects are so nicely self-contained, they should
be ideal candidates for serverless deployment. I guess one can’t easily
use Amazon Lambda, because the server is actually stateful (to persist
these client connections), but I could still imagine that some auto-
deploy-from-github-repo-without-configuration (like netlify etc.
offer), including data base etc. might be a nice add-on at some point,
so that it takes just minutes from writing your foo.urp to having a
live site.
## Summary
I was productive, in some sense, rather quickly, and I believe I could
finish this project (Not sure if I will, since it was a rewrite, I
don’t really get something new out of it, so my motivation is mild).
But it took me a bit longer than I hoped. I imagine if I didn’t have to
deal with Canvas, and maybe went for something less AJAXy and more
classical web programming with links and POSTs, I might have gotten
farther more quickly. Or maybe I just needed to practice a bit to get
up to more speed.
If I can’t resist and finish this, I’ll put the code online and let you
know :-)
Cheers,
Joachim
--
Joachim Breitner
mail at joachim-breitner.de
http://www.joachim-breitner.de/
More information about the Ur
mailing list