I have a ~1945 Walker-Turner 16” bandsaw, a truly beautiful machine. These things used to come with a gear box that would let you switch from cutting wood to cutting metal: a neat trick, but that apparatus was removed long ago, and it has only been used for wood (connected directly to a 1625 RPM motor, it runs at about the right speed). Since I started working with metal, I’ve wanted an easy way to chop up mild steel that doesn’t involve angle grinding or plasma cutting. I’ve also come into possession of a miscellany of gears, pulleys, flywheels, and most notably, a 10:1 reducer. Naturally, I’m wondering if I can combine these in some way to reduce the bandsaw speed to cut steel. Rather than manually calculating different potential configurations, let’s try and solve the general problem of “hooking up a motor of a given speed to a particular sequence of gizmos, what possible output speeds become available?” This is going to be overkill in any practical sense, but it’s a nice opportunity to apply some of Haskell’s equally-beautiful type system, and finally try using Template Haskell to create a domain-specific language (DSL).

What’s being modeled?

This is the first question: the physical objects here are spinning things, and connections between them. Basically, axles transferring rotation over belts, where the rotation speed can be modified in a couple of ways. Looking at the set of gadgets on my table, they seem to fall neatly into three categories: motors, which require no input and simply spin at a standard RPM, drives, where input to one end translates to the same RPM output at the other, and reducers, which are the same as drives except an internal gearbox changes the RPM by a fixed factor. Finally, axles need to have flywheels to drive the belts, and the diameter of the flywheel determines how much linear speed is created by a given RPM. So, let’s define datatypes that capture all these properties as intuitively as possible.

First, just for readability later on, we’re going to work with inches, and a flywheel is just a diameter, so:

type Inches = Double
type Flywheel = Inches

The three categories of axle are represented as a sum type:

data Gizmo = Motor { outDiam :: Flywheel }
	       | Axle { inDiam :: Flywheel
			      , outDiam :: Flywheel
				  }
		   | Reducer { inDiam :: Flywheel
				     , factor :: Double
		             , outDiam :: Flywheel
				     }

These are very intuitive: now we want to be able to hook a sequence of these together, feed a particular RPM in at one end (a Drive) and get the resultant speed (the out value of a Transfer or Reduce). I often find it helpful to write out the ideal invocation I’d like to be able to use, and work back from there. So, maybe I’d like to call:

getSpeed 1625 [Motor 2, Reducer 3 10 4, Axle 6 16]

to represent a 1625 RPM motor with a 2 inch wheel, attached to a 3 inch wheel on a 10-fold reducer driving a 4 inch wheel, attached to a 6 inch wheel on an axle driving a 16 inch wheel (this is actually very close to one possible setup for my bandsaw). What this function should tell me is how many feet per minute that final 16 inch wheel will be moving.

Each Gizmo ultimately passes along its rotation by imparting linear motion to a belt: namely, it’s current RPM times it’s out wheel’s circumference. Similarly, with the exception of Motor, a Gizmo gains rotation by receiving linear motion from a belt onto its in wheel’s circumference.

type IPM Double
getIPM :: IPM -> Gizmo -> IPM
getOutIPM ipm (Axle {..}) = rpm * outCirc
  where
    inCirc = pi * inDiam
	rpm = ipm / inCirc
	outCirc = pi * outDiam
	
getOutIPM ipm (Reducer {..}) = rpm * outCirc / factor
  where
    inCirc = pi * inDiam
	rpm = ipm / inCirc
	outCirc = pi * outDiam	
getIPM ipm (Motor {..}) = error "Motors have no input!"