The Frink is Good, the Unit is Evil
One day Alan Eliasen read a fart joke and got so mad he invented a programming language. 20 years later Frink is one of the best special purpose languages for dealing with units.
“But why do we need a language just for dealing with units?” Glad you asked!
Intro to Units
A unit is the physical property a number represents, like distance or time. We almost always are talking about SI units, or Système international. That’s things like meters, seconds, kilograms, along with prefixes like kilo- and mega-. One kilometer = 1000 meters, 1 centimeter = 1/100th of a meter.
Units can have exponents: m² is square meters, m³ is cubic meters, m * s-1 is speed. These form new physical properties, or dimensions. We can multiply and divide numbers with different dimensions but not add them. I can add 1 m + 1 m
, but not 1 m + 1 kg
. A unit-aware language needs to be able to track units and make sure that I don’t add incompatible dimensions. It also should also make sure that I pass correct units into functions that depend on units. A pressure calculation would take an area and a force, not a frequency and an amperage.
Simple, right?
Nope
First of all, there are multiple different representations of units. There’s the SI system, which all sane, right-thinking nations use, and then there’s the American system.1 Feet and meters are both distance. Is it okay to mix feet and meters? This is the bug that destroyed the Mars Climate Orbiter. But there are also valid uses cases where you’d want to mix them! When I cook, I use a mix of metric weights and American volumes, like 1 cup of water and 128 grams of flour. In the UK, beer is measured using pints but its alcohol by volume is measured in SI.
Different dimensions doesn’t always mean the units are incompatible. There’s a nonstandard set of units called “Gaussian units” used in some niches of physics. In SI, capacitance is measured in Farads, which has dimensions A² s⁴ kg-1 m−2. In Gaussian units, capacitance is measured in cm. If you’re explicit about what you’re doing you can add these seemingly-incompatible dimensions.
Dimensions aren’t unique, and two incompatible physical quantities can have the same dimension. The canonical example is that energy and angular force are both measured in Newton-meters.2 There are also plenty of domain specific examples. As you enter a gravity well, the rate at which gravitational force changes has dimensions N/m
. Surface tension is also measured in N/m
.
What else?
- What’s 200° + 360°? It could be 200° or 560°, depending on we have a “circular angle” or a “rotational angle” (like driving a screw in).
- Some quantities are unitless. What’s 20° + 1 radian? They’re both unitless quantities that have compatible dimensions. What about adding two ratios? What about adding a radian to a ratio?
- Units can have additional limitations. You can subtract timestamps from each other but not add them.
- Units can have different historical values. A foot is defined as exactly
0.3048
meters. Before 1959, though, it was0.3048006
meters. This is means that all historical documents have a different measurement of foot. And there was a period of time where we used both definitions in different places! - Uncertainties. If you’re working with physical measurements, all of your measurements will have some amount of uncertainty. What happens when you add two meters, plus or minus a centimeter?
If you want to learn more about how evil units are, check out Bill Kent’s Measurement Data Report.
Some solutions
Adding units of measure to a language is tricky. It’s not just that there are many hard domain problems, there’s also different tradeoffs in solutions. Do we want compile-time or runtime correctness? Should units be part of the type system or is there a better representation? How do we handle derived units? How do we handle generics? A good overview of the design challenges is here, which discusses some of the solutions proposed for Ada.
This page covers how various languages approach measurements. For the most part, they punt the question to preprocessors and libraries, limiting how much they can help you. The only mainstream language with built-in support for units of measure is F#. F# focuses on catching all errors at compile time. All conversions between units must be made explicit, so you can’t accidentally add feet and meters. The price for this is flexibility: you can’t add centimeters and meters, either.
// Mass, grams.
[<Measure>] type g
// Mass, kilograms.
[<Measure>] type kg
// Define conversion constants.
let gramsPerKilogram : float<g kg^-1> = 1000.0<g/kg>
// Define conversion functions.
let convertGramsToKilograms (x : float<g>) = x / gramsPerKilogram
That’s a good design choice for F#’s goals. It adds boilerplate but keeps your software from going haywire. This is appropriate for industrial software where a lot of people could be affected by a bug.
It’s less appropriate for low-stakes, small scale work. Boilerplate makes it harder to use things interactively. If I’m trying to find the Titanic’s weight in cups of rice I don’t want to spend ten minutes writing unit conversions. For that, the most common tool is units, a GNU program for conversions:
You have: 12 ft + 3 in + 3|8 in
You want: ft
* 12.28125
/ 0.081424936
units
is a fine piece of software, but it’s not a programming language. I want a language that has built-in dimensional analysis but doesn’t need a lot of boilerplate. Something more powerful than units
but not as strict as F#.
Frink
That’s where Frink comes in. In Frink, any number can be tagged with a unit. Frink will prevent you from adding incompatible units, same as F#.
> 1 meter
1 m (length)
> 1 meter + 2 kilograms
Error when adding: meter + 2 kilograms
Cause: Conformance Exception:
Cannot add units with different dimensions.
Dimension types:
m (length)
kg (mass)
The syntax is designed to look like “natural” math, so a space counts as a multiplication. 1 meter
is actually 1 * meter
. This may seem like it’ll cause some problems, but it’s actually kinda nice. It makes translating equations to Frink easier.
By using the ->
operator, we can convert a value between two units. We can choose whether we include the name of unit in the conversion or not by putting it in quotes.
> 10 meters + 2 furlongs -> "feet"
2029212500/1499997 (approx. 1352.8110389554112) feet
Frink comes with a boatload of predefined units; it’s default units file is over 5,000 lines long.
//How do you measure, oh measure a year?
> 525600 minutes -> years
0.99933688171353600106
> 525600 minutes -> siderealyears
0.9992981356527034257
> 525600 minutes -> gaussianyears
0.99926355644744010579
> 525600 minutes -> calendaryear
1
You can also define new units at runtime in terms of other units.
> corn_on_sale := 0.25 USD
// If we had a living wage, how many ears/corn
// could you buy with one days labor?
> 15 USD/hour * 8 hours -> "corn_on_sale"
480. corn_on_sale
> 15 USD/hour -> "corn_on_sale/(8 hours)"
480.0 corn_on_sale/(8 hours)
Prefixes in Frink are just multipliers. I can write kilofeet and it automatically knows that means “1000 feet”. Frink also knows common terms like half and square.
> square megapaces
5.80644e+11 m^2 (area)
You can break down conversions into multiple units.
> 1 kilometer -> ["kilofeet", "feet", "inches"]
3 kilofeet, 280 feet, 1280/127 (approx. 10.078740157480315) inches
> 1 kilometer -> ["kilofeet", "feet", "inches", 0]
3 kilofeet, 280 feet, 10 inches
Frink functions work as you’d expect, but you force parameters to conform to dimensions and preconditions.
> sphereVolume[radius is length] := 4/3 pi radius^3
> sphereVolume[2]
Error when calling function sphereVolume:
Constraint not met--value must have dimensions of length
Functions can also be the target of a conversion.
> HMS[1 day]
24 hours, 0 min, 0 sec
> (10 miles) / (3 mph) -> HMS
3 hours, 20 min, 0 sec
Affordances
Frink really shines in its affordances. It needs to be pleasant and reasonable if people are going to use it interactively. Frink does lots of small things to help with this. Some examples:
You can change the display units for a dimension.
> length :-> [feet, inches, 0]
// All lengths are now in feet and inches
> 1 meter
3, 3
// areas are unaffected
> 2 m * 2 m
4 m^2 (area)
You can globally set precisions and output formats.
> setPrecision[6]
> setEngineering[true]
140.5 million meters
140.500e+6 m (length)
You define entirely new dimensions and prefixes.
> population =!= person
> 2.7 megaperson / (5 square miles) -> (square feet) / person
Warning: reciprocal conversion
51.626666666666666665
By using ?str
you can get a list of all the existing units which start with str
. By using ??
it also gives you the value of the unit.
> ?foot
[acrefoot, arabicfoot, assyrianfoot, boardfoot, cordfoot, doricfoot, earlyromanfoot, foot, footballfield, footcandle, footlambert, frenchfoot, greekfoot, ionicfoot, irishfoot, lateromanfoot, northernfoot, olympicfoot, romanfoot, scotsfoot, sumerianfoot, timberfoot]
> ??foot
arabicfoot = 0.270256 m (length)
assyrianfoot = 0.27432 m (length)
boardfoot = 18435447/7812500000 (exactly 0.002359737216) m^3 (volume)
\\ it goes for a while
There’s special syntax for representing timestamps, which makes it easy to convert dates and times.
// Meeting is tomorrow, 3 PM Paris time, what's that in New York?
> # 3 PM Paris # + 1 day-> "New York"
AD 2020-07-10 AM 09:00:00.000 (Fri) Eastern Daylight Time
Some Use Cases
Eliasen loves cooking, so Frink has a lot of food measurements. A lot of baking recipes call for a volume of “sifted flour”, but sifting is messy, annoying, and imprecise. We should measure them by mass instead:
> ?flour
[breadflour_scooped, breadflour_sifted, breadflour_spooned,
cakeflour_scooped, cakeflour_sifted, cakeflour_spooned,
flour_scooped, flour_sifted, flour_spooned]
> 2.5 cups flour_sifted -> grams
283.49523125
Let’s do something more complicated. Like Eliasen, I also love cooking. While I mostly make savory dishes, I also specialize in chocolatiering. I’ve always been a little curious about how many calories each of my chocolates are, so let’s try to figure that out using Frink.
For this example I’ll use the Chai tea recipe from the CIA book.3 Here’s the list of ingredients:
Ingredient | amount (g) | serving size | Calories (kcal) |
---|---|---|---|
Heavy Cream | 180 | 1 tablespoon | 50 |
Milk Chocolate | 460 | 1.55 oz | 235 |
Corn Syrup | 60 | 2 tbsp | 120 |
Tea Blend | 10 g | - | - |
Butter | 20 g | 14 grams | 100 |
I pulled the calories of each ingredient from its label, with the exception of the milk chocolate, as I buy that in bulk. Instead I found information online here. Not the same kind of milk chocolate, but it’ll do for an estimate.
> specific_energy :-> "kcal/gram"
> energy :-> "kcal"
The :->
changes the default unit representation. Without it specific energy would be presented in base SI units, which would be joules per kilogram. Next I put all of the measurements into dictionaries. I didn’t bother converting every measurement to SI units, as Frink can calculate the ratios for me.
kcg = new dict // kcal/gram
kcg@"hc" = 50 kcal / (tablespoon heavycream)
kcg@"cs" = 120 kcal / (2 tablespoon cornsyrup)
kcg@"butter" = 100 kcal / (14 grams)
kcg@"mc" = 235 kcal / (1.55 oz)
recipe = new dict //what actually goes in
recipe@"hc" = 180 grams
recipe@"cs" = 60 grams
recipe@"mc" = 460 grams
recipe@"butter" = 20 grams
ing = ["hc", "cs", "mc", "butter"]
total_cals = 0 kcal
for x = ing
total_cals = total_cals + recipe@x kcg@x
Depending on the shell, this is enough for somewhere between 80 and 100 chocolates. We can model this uncertainty with an interval:
made = new interval[100, 120]
Instead of having an exact number, we have an upper and lower bound on the number. All math done with the interval instead change the bounds. 4
> x = new interval[80, 100]
[80, 100]
// frink does interval math
> x + 2
[82, 102]
> x * 2
[160, 200]
> x - new interval[80, 100]
[-20, 20]
But there’s more to a chocolate than its filling. There’s also the shell around it, which is going to have a different specific energy than the filling. To figure out how much shell there is per chocolate, I weighed 10 chocolates and took the average. That gave me 9 grams per chocolate. I know roughly how much of the filling there is per chocolate, based on how many chocolates I made from the filling. Subtracting the mass of the filling from the mass of the chocolate gives me the mass of the shell.
dictsum[d] := sum[map[{|x| x@1}, d]]
// grams per chocolate
gpc = new dict
gpc@"recipe" = dictsum[recipe] / made
gpc@"dc" = 9 grams - gpc@"recipe"
From then is a straightforward calculation to figure out how much calories both the shell and the filling have in total. And that gives me the total calories of the chocolate.
println[(total_cals / made) + kcg@"mc" * gpc@"dc"]
This tells me that each chocolate is 40-50 kcal, which is about what I expected.
If you’re interested in Frink, you can download it here. At the very least I’d recommend checking out the default units file; it’s a hilarious and insightful introduction to just how weird units can be. I’ll leave you with some of his comments on candela:
I think the candela is a scam, and I am completely opposed to it. Some good-for-nothing lighting “engineers” or psychologists probably got this perceptually-rigged abomination into the whole otherwise scientific endeavor.
What an unbelievably useless and stupid unit. Is light at 540.00000001 x 10^12 Hz (or any other frequency) zero candela? Is this expected to be an impulse function at this frequency? Oh, wait, the Heisenberg Uncertainty Principle makes this impossible. No mention for correction (ideally along the blackbody curve) for other wavelengths? Damn you, 16th CGPM! Damn you all to hell!
Thanks to Jubilee Young for making the opening image. Composite images are from Sean Connery in Zardoz and Alan Eliasen’s homepage.
- Blame Ronald Reagan. We were supposed to have completed metricization by the 90’s, then Reagan killed the program. [return]
- They aren’t exactly identical dimensions, as energy is a scalar and torque is a vector. But the vector components of torque have the same dimensions and are scalars. [return]
- The other CIA. [return]
- Frink treats intervals as a single unknown value inside the interval, not the range itself. This means that for any interval
x
,x - x = 0
. [return]