4
\$\begingroup\$

I have been working on this Lorenz Attractor visualization for the past day. I am currently also trying to change my coding style into a more functional programming one.

I find it quite hard, to be honest, especially the "Only use pure functions." rule. That mostly means no side effects and functions that perform 1 small task. The "No side effect." rule is, of course, impossible while drawing on the HTML canvas. So I am looking for performance optimization and code style tips for FP in general.

I also should like to pass the 3 global variables (x, y, z) as parameters when calling the update function ("No global variables allowed.") however I have had no luck so far. I can't find a way to keep them from resetting while using parameters. Unlike the draw function that calls itself. Any help is welcome, thanks for your time.

https://jsfiddle.net/9r8qcgnh/3/

{
  const canvas = document.querySelector('#canvas')
  const ctx = canvas.getContext('2d')
  ctx.fillStyle = 'rgb(255, 255, 255)'
  ctx.translate(500, 400)
  ctx.scale(10.5, 10.5)

  let x = 0.01
  let y = 0.0
  let z = 0.0

  function update () {
    const a = 10.0
    const b = 28.0
    const c = 8.0 / 3.0
    const dt = 0.015
    const dx = a * (y - x) * dt
    const dy = (x * (b - z) - y) * dt
    const dz = (x * y - c * z) * dt
    x = x + dx
    y = y + dy
    z = z + dz
    return { x: x, y: y, z: z }
  }

  function draw (hue) {
    const { x, y } = update()
    hue = hue || 0
    hue >= 240 ? (hue = 0) : hue++
    ctx.fillStyle = `hsl(${hue},100%, 50%)`
    ctx.fillRect(x, y, 0.1, 0.1)
    setTimeout(() => {
      return draw(hue)
    }, 5)
  }

  draw()
}
\$\endgroup\$
2
\$\begingroup\$

I don't know anything about Lorentz Attractors or functional programming, but I have a few things I want to mention.

getElementById is faster than querySelector

const canvas = document.getElementById('canvas')

You can use shorthand property names in your draw function.

return { x, y, z };

You can give parameters a default value

function draw (hue = 0) {
  //...
}

and remove this line

hue = hue || 0

I don't know if it's deliberate because you have something against purple, but the hue goes to 360.

Use semicolon at the end of your lines. Technically, JavaScript can run without them, but it just... looks wrong. If anything, do it to make it a habit, since it's gonna be a problem if you code something in another language later.

It seems more appropriate to use setInterval instead of setTimeout here. You will have to handle the hue differently in that case. Making it global is an option, but not good if you are trying to avoid those. Another option could be a generator.

function draw (hue = 0) {
  const { x, y } = update();
  ctx.fillStyle = `hsl(${hue},100%, 50%)`;
  ctx.fillRect(x, y, 0.1, 0.1);
}

const hueGenerator = (function*() {
  let hue = 0;
  while(true) {
    hue %= 360;
    yield hue++;
  }
})();

setInterval(() => {
  draw(hueGenerator.next().value);
}, 5)
\$\endgroup\$
1
\$\begingroup\$

The "No side effect." rule is, of course, impossible while drawing on the HTML canvas.

States and side-effects are the two things FP avoids, but are effectively what defines a program. I like to tell people that state and side-effects will always exist but "is just pushed away somewhere else in the code/language". If you push them away, what you'll be left with are stuff that can be written purely. For instance:

function update () {
  const a = 10.0
  const b = 28.0
  const c = 8.0 / 3.0
  const dt = 0.015
  const dx = a * (y - x) * dt
  const dy = (x * (b - z) - y) * dt
  const dz = (x * y - c * z) * dt
  x = x + dx
  y = y + dy
  z = z + dz
  return { x: x, y: y, z: z }
}

a, b, c and dt are all constants. You can pull them out of the function, they don't need to be redefined on every update. You can either define them outside (totally fine, as long as their values don't change), or create a closure (a function that returns another function).

You need not update x, y, nor z in this function, just return them. Push the side-effects away for now. Focus on generating new values from old values.

hue = hue || 0
hue >= 240 ? (hue = 0) : hue++

Your hue computation can be placed inside a pure function - a function that accepts your previous hue and returns the new hue.

setTimeout(() => {
  return draw(hue)
}, 5)

Use requetAnimationFrame instead. This has a bunch of benefits over timers, like allowing you to animate smoothly in 60fps (or whatever your screen refresh rate is).

Here's my attempt at making everything FP-ish. Everything that can be written purely are pulled out into their own functions. Side-effecty operations are pushed into their functions as well. Note that there's no vars or lets or reassignments.

{
  // Reference to our canvas
  const canvas = document.getElementById('canvas')
  const ctx = canvas.getContext('2d')

  // Function accepts a, b, c and dt, and returns a function that accepts x, y and z
  // which you can call to generate new x, y and z.
  const getCoordsFactory = (a, b, c, dt) => (x, y, z) => ({
    x: x + (a * (y - x) * dt),
    y: y + (x * (b - z) - y) * dt,
    z: z + (x * y - c * z) * dt
  })

  // Create a function with the given initial values for a, b, c and dt.
  const getCoords = getCoordsFactory(10.0, 28.0, 8.0 / 3.0, 0.015)
 
  // getHue accepts a hue value and either increments by 1 or resets to 0.
  const getHue = hue => hue >= 240 ? 0 : hue + 1

  // Updater function. Accepts previous values, gets updated values, and
  // "recursively" calls itself with the updated values. Your "state"
  // is now args of a recursive call.
  const loop = (x0, y0, z0, hue0) => {
    // Get updated values
    const { x, y, z } = getCoords(x0, y0, z0)
    const hue = getHue(hue0)

    draw(x, y, z, hue)
  
    // Schedule next draw. Looks recursive, but it's actually async.
    requestAnimationFrame(loop.bind(null, x, y, z, hue))
  }
  
  // All the side-effecty operations pushed into 2 functions.
  
  const draw = (x, y, z, hue) => {
    ctx.fillStyle = `hsl(${hue},100%, 50%)`
    ctx.fillRect(x, y, 0.1, 0.1)
  }
  
  const reset = () => {
    ctx.fillStyle = 'rgb(255, 255, 255)'
    ctx.translate(500, 400)
    ctx.scale(10.5, 10.5)
  }
  
  reset()
  loop(0.01, 0.0, 0.0, 0)
}
body {
  margin: 0;
}

canvas {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  background: #000;
}
<canvas id="canvas" width="1000" height="800"></canvas>

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.