DEV Community

loading...
Cover image for try Scheme instead of JavaScript for UI

try Scheme instead of JavaScript for UI

rodiongork profile image Rodion Gorkovenko ・5 min read

In this tutorial we'll try create animation
LIKE THIS (Bunny image bouncing inside rectangular DIV) - using funny language Scheme instead of JavaScript.

Why trying Scheme?

Besides being fun and with curious syntax, it greatly influences your coding style. It is very hard to write bad code in Scheme/LISP because
you'll get lost in the heaps of parentheses. So it makes you arranging code in small and clever functions (we call it "decomposition").

Also, if you go in depth, it teaches a lot about functional programming, especially with its list data types.

What is Scheme

Scheme is functional language with very simple syntax. It is one of main branches of LISP - which is the oldest language still being used widely enough. (Fortran is older, but not used that much nowadays) If you heard of Clojure, Guile, Common Lisp - you know they are variants.

It looks like this:

(alert (+ 5 8))
Enter fullscreen mode Exit fullscreen mode

Note two things:

  • all language constructions are simply made of list elements inside parentheses, or round brackets, i.e. ( ... )
  • all function names and operators are put immediately after opening bracket

So our example simply calculates sum of 5 and 8 and shows it with browser alert box (it is for browser version, of course)

We will use Scheme implemented in JS, called BiwaScheme - as we want it for UI tasks. You can try the expression above in the interpreter on this page.


Let's start - Page Setup

I'll do everything in single html file, for clarity - though you can split it later, of course. Let's make <div> with <img> of the bunny inside:

<div id="field">
    <h1>Heeey, I'm Bouncing Bunny!</h1>
    <img id="bunny" src="https://codeabbey.github.io/js-things/bunny/bunny2.png"/>
</div>
Enter fullscreen mode Exit fullscreen mode

And add some coloring and form (you may see, final demo fits any window, but for now let's use fixed size of the DIV):

<style>
    body {
        background: gray;
        text-align: center;
    }
    #field {
        background: cyan;
        margin: 0px auto;
        position: relative;
        width: 640px;
        height: 480px;
    }
    #bunny {
        position: absolute;
        height: 100px;
    }
</style>
Enter fullscreen mode Exit fullscreen mode

At last let's add some stub for script with Scheme. You see, we use biwascheme.js (release from the site mentioned above) in src attribute and the code itself inside tags (which is a bit unorthodox) - but that's not the only method. This code will find H1 element and set its color to red, just as we can do it with jQuery:

<script src="https://codeabbey.github.io/js-things/biwascheme.js">
(set-style! (get-elem "h1") "color" "red")
</script>
Enter fullscreen mode Exit fullscreen mode

Put all three snippets into file and check if it works.

You should see something like this.


Variables and Bunny positioning

Global variables are not very good in functional code, but the task of animation is also not very "functional" one, so let's add X and Y to mark current position of the bunny. This and all the following should be put inside <script> stub we created above:

(define x 0)
(define y 0)
Enter fullscreen mode Exit fullscreen mode

Now we also would like to have some auxiliary variables - to hold field and bunny DOM objects - and also for sizes of the field (while sizes of bunny are not calculated before image is loaded, we'll work around this). For such things we have several simple functions which are not in standard Scheme, but were added in BiwaScheme for interoperability with JavaScript:

(define field (getelem "#field"))
(define fieldW (element-width field))
(define fieldH (element-height field))

(define bunny (getelem "#bunny"))
Enter fullscreen mode Exit fullscreen mode

At last let's add function for setting coordinates of the Bunny - and add test-call to it, to check it works. It works just by assigning top and left styles of the bunny element we got above:

(define (bunnyMove left top)
    (set-style! bunny "left" left)
    (set-style! bunny "top" top))

(bunnyMove 50 50)

Enter fullscreen mode Exit fullscreen mode

This code doesn't have a lot of observable changes compared to previous step - but the bunny should be off the center line to prove coordinates and moving works really.

See here how it should look, please.


Timer and Movement

So now we need to setup something like setInterval(...) to change coordinates over time and update bunny position. Final example uses requestAnimationFrame instead, but let's go on with timer now for simplicity. BiwaScheme includes simple analog of setInterval:

(set-timer! updatePosition 0.05)
Enter fullscreen mode Exit fullscreen mode

Here set-timer! specifies delay in seconds (so it is 0.05, i.e. 50ms - 20 frames per second). We are going to call updatePosition function on timer ticks. But we need to add it of course:

(define updatePosition
    (lambda ()
        (set! x (+ x 1))
        (set! y (+ y 1))
        (bunnyMove x y)))
Enter fullscreen mode Exit fullscreen mode

Here we see that syntax for defining function is the same as for variable - we just assign lambda-function (with no arguments) to the updatePosition variable. Scheme also has simplified syntax for the same, as you can see in bunnyMove definition.

Add these snippets to the end of your script and see - bunny should start moving, though ignoring bounds of the field.

Look here for example, please...


Regard Borders and Speed-up

To make Bunny respect borders of the field, we'll need to teach it changing direction. For this we should add something instead of 1 to its
coordinates. Let's create variables for vertical and horizontal speed, and use it in updatePosition:

; put this below x, y definition
(define vx 2)
(define vy 2)

; and later...
(define (updatePosition)
    (set! x (+ x vx))
    (set! y (+ y vy))
    (bunnyMove x y))
Enter fullscreen mode Exit fullscreen mode

Now we just need to check inside the updatePosition for following conditions:

  • if bunny reaches bottom, i.e. y >= field_height - bunny_height - then switch vy to negative
  • if bunny reaches top, i.e. y <= 0 - then switch vy back to positive
  • make two similar checks for x coordinate
(define (updatePosition)
    (if (>= y (- fieldH (element-height bunny))) (set! vy -2))
    (if (>= x (- fieldW (element-width bunny))) (set! vx -2))
    (if (<= y 0) (set! vy 2))
    (if (<= x 0) (set! vx 2))
    (set! x (+ x vx))
    (set! y (+ y vy))
    (bunnyMove x y))
Enter fullscreen mode Exit fullscreen mode

Note that we call function to calculate bunny size every time. This is workaround because we couldn't get it until bunny image is loaded. Of course it could be done in more clever way, but let's keep it simple.

Surprisingly, that's all! Now bunny should bounce of all 4 borders.

Look how it works in the last example


Conclusion

So we see, Scheme looks not too bad when dealing with JavaScript tasks! I can't tell for sure how good it is with larger programs - I just decided to try it myself.

The "final" version of the code, linked at the beginning of this post, is slightly advanced - adapting to various screen sizes and using requestAnimationFrame and time deltas. You may study it, but probably it is too obvious and you even know how to do this better.

I hope that even if you won't write much in LISP/Scheme - you still can try it for amusement, and to learn a bit more about different programming languages!

Thanks for reading that far and Happy Coding!

Discussion (4)

pic
Editor guide
Collapse
deciduously profile image
Ben Lovy

Wholeheartedly agree - LISP and frontend is a great match. I'd never thought to do it with Scheme, will absolutely be trying this out.

I also want to shout out this series by @kendru about using ClojureScript in a JS context.

Collapse
rodiongork profile image
Rodion Gorkovenko Author

Thanks! Never heard of this, very interesting!

Though at first glance - it requires compilation, isn't it so? This makes solution a bit less handy, like "Go to JS compiler" - though I'm sure one can get used to it :)

Also it seemed for me Clojure is more syntax and feature rich dialect of LISP (while Scheme is rather minimalistic). I failed to advance far in it at first attempt years ago. I'll try again, thanks! :)

Collapse
deciduously profile image
Ben Lovy

Yes and yes, both great points. You indeed compile CLJS to JS, it uses the Google closure compiler under the hood. This is both a pro and a con - it's nice that what you ultimately ship is just plain ol' ES5 JS, but you do need a build step. The lisp is just for the developer.

Scheme is also much more minimal than Clojure. They do have different design goals. I like Clojure's built-in fancy stuff, the rich set of data types alone is a selling point over Scheme for me. There is objectively more language, though, so it depends on your goals and personal taste.

Collapse
jcubic profile image
Jakub T. Jankiewicz • Edited

You should look at LIPS my Scheme written in JavaScript, I'm close to releasing version 1.0 need to find more time and some motivation to work on it.

When version 1.0 will be ready I will create proper intro article that showcase all the features.
What's cool is that you don't need to rely on eval and bunch of functions written in JavaScript like in BiwaScheme.

Version 1.0 that I have already on devel branch (will release some beta version soon) have syntax like this (document.querySelector "div"). My scheme also have hygienic macros that still need to some work.

Some comparison between current version that is not master and npm:

(--> document (querySelector ".terminal") 'style (setProperty "--color" "red"))
Enter fullscreen mode Exit fullscreen mode

version 1.0

(--> (document.querySelector ".terminal") (style.setProperty "--color" "red"))
Enter fullscreen mode Exit fullscreen mode

and all that is just --> macro and basic syntax. The rest are standard JavaScript functions in the browser.

LIPS can work with any library and have only one small dependency which is bn.js that is needed if you want to have big number support.

Did I say that even that is custom lisp it's almost 100% compatible with R5RS specification? I hope it will also be more compatible with R7RS that I've started implementing.

BiwaScheme was great but because of it's implementation (byte code) it's hard to fix bug and adding new features.

And one more thing one of the demos is Preact (slim React) app written in LISP.

I have lot to do before and after version 1.0. Like I'm in process of creating Dev tools extension that will give REPL to LIPS apps. Will also create a blog on GitHub pages, where I will write tutorials that will explain how to actually write stuff. I was also thinking about helping people learn Scheme by adding Bookmarklet that add REPL to any website that have Scheme tutorial. But first I need to release version 1.0. I think that blog with RSS will be much sooner.