I seem to have renewed my collaboration for a tablet-friendly programming environment. For the current version, my partner led and pushed for preserving LÖVE primitives. For example, the LÖVE event loop calls functions you define called love.draw, love.keypressed, etc., and it seems desirable to continue to support those for familiarity's sake. This can seem impossible if the environment is also built in LÖVE and uses those same functions for providing its infrastructure, but since Lua is a dynamic environment there are tricks to make it work reasonably nicely.
Lately I've been taking the lead on a riff of that project, and I find I'm advocating strongly to not do this. I want to define new handlers like shell.draw and shell.keypressed that programs within my environment will have to call. And I finally figured out why I feel so strongly about this:
LÖVE has a function to return the bounds of the device/window. You typically can draw between x coordinates 0 and some width. And this information is often used to draw UI elements near the edges and corners. So should we now override functions like line to adjust coordinates and keep them in the client area? There's a lot of surface area to cover here. Bugs will inevitably happen, and when they do our attempts to create a seamless abstraction will cause more confusion.
Ranting more broadly, the desire for a "seamless experience" is a disease. The provider wants to "own the customer relationship," so that the user can forget there's anything in the universe but themselves and the app. Just like a casino. I'd rather not pretend we can wish the universe away with its possibility of errors. Let's me and the user treat each other as grown-ups, and not hide irreducible complexity in our supply chains. Here are the functions I provide, here are the functions LÖVE provides. I recommend you use my stuff where possible, but you don't have to. Yes, it's confusing. Welcome.
I have to be careful to check the results of coroutine operations, because the underlying coroutine might have thrown an error.
Errors in Lua include a call stack, but errors within coroutines don't return the stack by default.
If I create a higher-order helper to abstract away the coroutine munging just to smear a computation across frames, does that impact the quality of debug information in the call stack? (Answer: no it doesn't in Lua, but it wasn't obvious.)
Call stacks returned by LÖVE aren't quite as clean as plain Lua.
I spent some time today playing with Poisson Disk Sampling (image 1), and I tried hard to avoid using any `print`s to the terminal, which forced me to create some nice debugging UI (image 2 and 3).
Today I played around with a little parsimonious universe for exploring easy versions of certain problems. I've had trouble with stuff like this in the past, and this time I'm hoping to make more progress by:
simplifying the problems to this parsimonious "Rectangle world" setting;
starting out really easy and gradually building up to harder problems; and
building up some tools for visual debugging along the way.
I've been doing a lot more graphical and numerical programming lately, and I've been reminded of some of the unique challenges such programs pose.
It's hard to write automated tests for them.
Also, errors can be subtle. It's very difficult, when I'm tracing through a program, to stare at a line of code and a value of a variable and tell if it's right or wrong.
So I've been thinking about how I can improve the debugging experience for myself, now that I have this live programming environment. I feel like I'm not using it to its full potential.
Here's a very simple testbed program. I press a key, it performs some computation and it prints a value on the on the screen.
Let's look at the code for it.
As I said, it's printing a result to the screen and the result is computed on a keypress. And this is the computation over here.
The result is computed in a single shot. But if I insert a line that looks like this, now I can see the intermediate results render as the computation is performed.
This is kind of nice and of course, we don't have to be limited to textual rendering. It's like debug print but with greater expressivity.
However, there are some drawbacks. For one, I keep finding myself pushed when I program in this style, to not have local variables. Any intermediate result I might want to render wants to be a global variable.
That's why the result of the computation is a global. Instead of saving the result to the global in the caller, which makes the computation purely functional, I need to compute directly into the global.
I'm using coroutines under the hood and by writing directly to the globals, I don't have to think too hard about how to resume the coroutine. I can treat arbitrary computations the same way. Each coroutine is responsible for its side effects.
But again, globals all over the place, which makes me nervous.
On my Android web browser, I can download a .love file and open it.
After downloading the file, I can open it in my file browser.
I can copy the file into Android using the USB port -- but I CANNOT open it there. Identical bits in storage.
And mobile browsers don't understand file:// So I need the internet to transfer data between two devices right next to me -- even WHEN I have the right wire.
Local-first?! I'd settle for local-somehow-anyhow.