Tuesday, March 31, 2015

Using juxt to Compute Signed Angular Distances

Juxt is one of those weird Clojure functions that just doesn't make sense until you see it in action. However, once you get it, you love it. This short example shows a great use of the juxt function.

What does juxt do? It creates a function that is the application of a list of functions to a single argument. In simpler terms, a typical function returns one result for one input. In the case of juxt, you create a function that returns n results for one input. The n results are computed from the n functions that you juxtapose.

For example, suppose you need both the cosine and sine x for a given application. You could create two separate variables, like so:
(defn angle->cartesian
"Convert an angle (in radians) to its cartesian vector 
equivalent."
  [theta]
  (let[x (Math/cos theta)
       y (Math/sin theta)]
    [x y]))

However, it is more convenient to juxtapose (set side by side) the two items being computed into a single vector.

Here's a problem that builds on the above to show how nice juxt is:

You are given two angles in degrees and need to compute the signed distance between them. These angles aren't constrained to [0, 360). If the angles are outside of this range, we want to use the equivalent angle within the desired range. Here are a few examples of the desired output:

First AngleSecond AngleSigned Distance
102010
2010-10
3501020
10350-20
-350350-20
350-35020

You can probably solve this problem with some combination of mod, rem, and/or quot, but I find it much easier to simply transform the angles to their cartesian vector form and do the math in that coordinate system. Recall that for unit vectors the dot product of vectors A and B is A*B=cos(θ), where θ is the angle between the vectors. Likewise, the sign of the cross product of the vectors indicates the the direction of rotation via the right hand rule. So, I can use dot to compute my angular offset and cross to determine the direction.

With that knowledge, here's my function to compute angular distances:
(defn angular-delta
"Compute the difference between two angles, 
where angles are in degrees."
[a b]
(let [[u v] (map (juxt #(Math/cos %) #(Math/sin %))
                 (map #(Math/toRadians %) [a b]))
      theta (Math/acos (reduce + (map * u v)))
      sign (Math/signum (reduce - (map * u (reverse v))))]
  (Math/toDegrees (* sign theta))))

Pretty slick, eh? Notice how the combination of juxt and destructuring (the automagic mapping of the result to the [x y] vector) both shortened my code and created a single vector structure, which communicated my intent. As an afterthought, let me state that I am only converting to degrees since they are easier on the eyes for most humans. In the real world, I'd leave the function in terms of radians since, in the words of my favorite college math professor, "Degrees are for temperature, radians are for math."

Does this all give the right answer? Let the REPL decide:
(defn angular-delta
  "Compute the difference between two angles, 
  where angles are in degrees."
  [a b]
  (let [[u v] (map (juxt #(Math/cos %) #(Math/sin %))
                   (map #(Math/toRadians %) [a b]))
        theta (Math/acos (reduce + (map * u v)))
        sign (Math/signum (reduce - (map * u (reverse v))))]
    (Math/toDegrees (* sign theta))))
=> #'user/angular-delta
(angular-delta 10 20)
=> 10.000000000000012
(angular-delta 20 10)
=> -10.000000000000012
(angular-delta 350 10)
=> 20.00000000000001
(angular-delta 10 350)
=> -20.00000000000001
(angular-delta -350 350)
=> -20.00000000000001
(angular-delta 350 -350)
=> 20.00000000000001

Ignoring the precision issues, this gives just the right answer.

I hope this provides a nice explanation of how you might use juxt in a real problem. There are many other similar cases where you need to compute several independent variations of an initial value to perform a computation. Whenever you need do to this, use juxt.

If you liked this page or learned anything from it, please tweet the link and/or follow me on Twitter. Your support and interest is the fuel that drives the effort.

No comments:

Post a Comment