You see, you immediately had to revert to "its square" (to finally get to the "voltage squared over Herz")... as the square root of W over Hz
thing doesn't make much sense physically.
@bbrk24 got a point here:
If to incorporate that, then instead of:
var upper: [String: Int]
var lower: [String: Int]
I'd use:
struct Rational8 { var numerator, denominator: Int8 }
var power: [String: Rational8]
and the example presented would become:
["V" : Rational(1, 1), "Hz": Rational(-1, 2)]
Perhaps not a String though, but an enum:
enum BaseUnit {
case mass, length, ...
}
or if you consider enumeration too rigid - an enum-like struct:
struct BaseUnit {
var rawValue: Int8
static let mass = BaseUnit(rawValue: 0)
static let length = BaseUnit(rawValue: 1)
...
}
The dictionary usage seems quite "heavy"... I'd search for a fixed size alternative, even at the price of the whole solution being not so extendable. Example:
struct Unit {
var mass: Rational8
var length: Rational8
...
// like 10 of these fields, or how many do we have
}
The main type itself (called MyUnits in your sources) is better be a value type, not a class. All IMHO.
I don't understand what you're accusing me of here.
For now let's assume the physical quantity is in fact W/Hz
. Even if we store it like that, at some point the user is going to want to see the more common V/√(Hz)
. If we don't allow sqrt-units, this becomes more complicated:
let noiseSquared = // something in W/Hz, or perhaps an equivalent unit
print(noiseSquared.squareRoot()) // V/√(Hz), or perhaps an equivalent unit
// vs
let noiseSquared = ...
let noiseQuantity = noiseSquared.convert(to: .wattsPerHertz).value.squareRoot()
print("\(noiseQuantity)V/√(Hz)")
In case it's interesting, I've developed a swift package called Physical that does many of the things discussed here, including advanced handling of combined quantities with rational exponents and more. Please check out the examples on the repo page, see what you think. I have an more advanced version of it in development. Happy to get feedback and help with this effort.
Thanks for sharing.
I can see both this:
public struct FundamentalBaseVector {
public var m = 0
public var s = 0
public var kg = 0
public var A = 0
public var mol = 0
public var K = 0
public var cdl = 0
public var rad = 0
public var st = 0
And this:
public typealias DimensionDictionary = [Dimension : (unit: Dimension, exponent: TieredNumber)]
Don't you think the first type would be enough if only you used Rational
(or your TieredNumber
) for the power instead of Int
?
A few comments:
8√14 // 8th root of 14. can be any integer.
This could be confused with 8 * √14
.
14.0 ^ 2
- somewhat error prone as could be confused / mistyped as 14 ^ 2
4.π²
- this took a while for me to parse, and I am not sure still: is this (4*π)^2
or 4 * π^2
? Not obvious. BTW, will the -x²
result be -(x^2) or (-x)^2 ? I guess it is the latter (although it's not obvious and where Swift and math disagree). Note that we can not specify the precedence of postfix/prefix operations in respect to prefix / postfix / infix operations.
I'd trap instead (ideally it should not compile, but this could be too much to ask).
var distance1 = 10.5.centimeters
let distance2 = 3.3.feet
distance1 + distance2
What's the result dimension here, is it centimetres? and is it feet for distance2 + distance1
? Not my cup of tea but I can understand the desire. I'd make those unit conversions more explicit (and would prohibit mixing different units).
(distance2 / speed).to(.hours)
Just checking, what would happen if I put .meters
instead of hours? Ideally that should be a compilation error, less ideally a runtime trap:
(distance2 / speed).hours // time in hours
(distance2 / speed).meters // a compilation error
Yep, overall I found the solution somewhat heavy on using those hard to type symbols. Call me asciiterian.
let aForce = 1.kilograms.meters.seconds(-2)
Nice. I guess this could work as well?
let aForce = 1.kg.m/.s²
or even this?
let aForce = 1.kg.m.s⁻²
Overall I like it, thanks again for sharing, there are a few great ideas here.
thanks @mgriebling! also: ha! thanks for the catch.
hi @tera, thanks for the feedback!
One unfortunate thing about super- and subscripts is that they cannot be used as postfix operators, so s²
could be hard-coded to s(2)
but since most people can't directly type ²
anyway, I don't make type sub- or superscripts required, just odd conveniences. (I used a customized keyboard layout.) So, for instance, π²
is just a label for a constant, so 4.π²
is 4 * pi * pi
. Since π²
comes up in many physical equations, I thought it might be convenient (and more findable via autocomplete than a bare ²
is).
For sure things like 8√14
could be confusing, and was really meant as a convenience for the more-likely cube root, 3√x
, vs the pain of finding the ∛
symbol or having to write ^(1/3)
.
The FundamentalBaseVector
is purely a description in terms of the base units of each dimension, whereas DimensionalDictionary
is more general, storing things in terms of Watts
or ergs
. They are related but the former can't replace the latter.
distance1 + distance2
only retains non-standard base units if both items share them, otherwise the result is internally stored as a standard base unit, in this case meters. So in that example above, the result is in meters.
The system does not strong type 10.5.cm
currently, but this is something i keep going back-n-forth about. Length(10.5, unit: .centimeters)
is, of course.
I try to avoid anything that traps (I assume you mean crash). I don't want any user to have to worry about the package itself crashing ever. It should provide enough type info to only crash if the user has put themselves into that position thru their algorithm choices.
About your .to(.meters)
question, (distance / speed).to(.meters)
will return .notAThing
(a la NaN
). If the variables are generally typed, the compiler can't know ahead of time what their units are, and I do not want that to crash an app. There are several ways to check at runtime, including the ~
test or trying to strong-type it via if let d = Length(distance / speed)
. If it is strongly-typed then you'll get a nice compilation error. (Side note: there is a bug on this specific operation that need to get cleaned up.)
I enjoy pushing the envelope on what should be typable, but I understand others don't, and so no non-ascii characters are required, but are there for those who want to have fun with them. I've gotten equally positive and negative reactions from people. (Also thanks for verifying that some people read to the end! First time someone's acknowledged my neologism there.)
Thanks for the feedback and probing questions! I'll think about it more over time.
I see. Quite unfortunate indeed.
Could a superscript be used here? ⁸√14
. Would be cool, but I fear it's not possible.
Can you clarify more on this. For example to represent Watt I could use something like this:
FundamentalBaseVector(m: 2, s: -3, kg: 1)
or the equivalent notation that takes rational powers for base units. Perhaps some "scale: 0.001" parameter to specify milliwatts instead of watts. What am I missing?
It could be a big deviation from your current method; I am thinking along the terms of more "static" API, for example:
func / (lsh: Length, rhs: Speed) -> Time { ... }
extension Time {
var minutes: Time { ... }
}
extension Length {
var centimeters: Length { ... }
}
Here attempt to call centimeters
on Time
(or calling minutes
on Length
) would be a compilation error.
Can you clarify more on this. For example to represent Watt I could use something like this:
FundamentalBaseVector(m: 2, s: -3, kg: 1)
or the equivalent notation that takes rational powers for base units. Perhaps some "scale: 0.001" parameter to specify milliwatts instead of watts. What am I missing?
The system doesn't auto-convert into fundamental base units. So 12.watts
will be stored as watts until such time that a decomposition and conversion of units is necessary. This is attempt to maintain numerical accuracy and lighten unit conversion operations as much as can be. Most code only rarely requires unit conversions. I'd hate for someone constantly using fahrenheit measurements to have to waste tons of cpu time converting to Kelvin for no gain and possible loss of accuracy. FundamentalBaseVector
representations only get used during a decomposition process.
Also, in the future I would like to make the entire selection of fundamental units swappable, and would like to retain this abstraction separation.
This isn't a bad instinct, but now we have a combinatorics problem. We would need 34! (~3e38) different /
methods to handle all the dimension combos, and it would still fail us for any custom dimensions the users append for use in the system (this latter feature of the system is still in-progress).
That said, perhaps something can be done along these lines, if even only for common operations.
I see. Just imagine how "light" the type could be if it is pure struct with a dozen integer fields, and no dictionaries, internal reference variables or other lock takers... That could really fly... That could be worth fighting for.
Unit(m: 1, s: -1) * Unit(s: 1) ---> Unit(m: 1)
As @bbrk24 pointed out the powers need be rational.
I am disregarding the milli / killo or Fahrenheit / Kelvin aspects for a moment to show the bigger picture.
I shall be open-minded about this and see what can be done. I consider the whole thing to be in its early days and I appreciate the nudge toward simplicity.
Currently yes, but I want to make that more flexible in the future. (Back when Numerics was launched, I converted the whole framework to use it, but found that it introduced a number of type issues that I was unhappy about, so I pulled it all back out again.)
This is where Swift could learn from C++, minimal illustrating example:
#define let const auto
#define var auto
template <int Meter, int Second, int Kilogram>
struct Unit {};
template <int M, int S, int KG>
Unit<M,S,KG> operator + (Unit<M,S,KG> lhs, Unit<M,S,KG> rhs) {
return Unit<M,S,KG>();
}
template <int M1, int S1, int KG1, int M2, int S2, int KG2>
Unit<M1+M2,S1+S2,KG1+KG2> operator * (Unit<M1,S1,KG1> lhs, Unit<M2,S2,KG2> rhs) {
return Unit<M1+M2,S1+S2,KG1+KG2>();
}
template <int M1, int S1, int KG1, int M2, int S2, int KG2>
Unit<M1-M2,S1-S2,KG1-KG2> operator / (Unit<M1,S1,KG1> lhs, Unit<M2,S2,KG2> rhs) {
return Unit<M1-M2,S1-S2,KG1-KG2>();
}
typedef Unit<1,0,0> Length;
typedef Unit<0,1,0> Time;
typedef Unit<0,0,1> Mass;
typedef Unit<1,-1,0> Velocity;
typedef Unit<1,-2,0> Acceleration;
let a = Length() / Time(); // ok
let b = Length() + Length(); // ok
let c = Length() + Time(); // comiltaion error
Time d = Length() + Length(); // compilation error
Mass e = Length() / Time(); // compilation error
Could be generalised to use rational powers.
yeah, units should really be a ZCA in the type system, doubling the size of any dimensioned quantity just to carry around an extra “units” field doesn’t seem like the right solution here.
hopefully once type packs land in the language, we might be closer to a real compile-time units system.
I love using Apple's existing Measurement
feature. Currently I've been writing (for the fun of it) a package to model sun position. What I find very useful about Measurement
is I don't need to worry about making sure I'm using degrees/radians or various magnitudes.
I've added a few things so I can do trig on UnitAngle
, even inverse trig which returns a UnitAngle
.
What I have wished for though is the ability to convert between dimensions using a property e.g. myAngle.degrees
rather than myAngle.converted(to: .degrees).value
, as well as more terse ways of creating a value.
While multiplying different units would be tremendously useful, I can't see how the number of combinations could be catered for. Perhaps the easiest way is for custom units created as and when needed, following some convention, then these contributed to the open source project?
What would be good to start with is some basic representation of rate, such as per time, per length, per area.
Most units can be defined in terms of fundamental units (
Force = Mass * Acceleration
, butAcceleration = Distance / (Time * Time)
, soForce = Mass * Distance / Time * Time
).
On a tangent, here's something most people don't realise (possibly because I might be wrong)…
Ancient civilisations believed matter consisted of earth, water, fire, air. Some might even say there was a mystical 5th element. But let's not go there.
It occurred to me that these ancient people were surprisingly right. As far as I can figure, all SI units derive from 3 of those 4 starting points.
- The metre originally derived from the earth's circumference (10000km from the equator to the pole)
- The second is derived from the earth's rotation
- 1000kg was the mass of 1 cubic metre of water
- Kelvin comes from centigrade which derived from water
- Candela comes from the brightness of a candle (fire)
I believe every other unit is a based on those 5. For instance, IIRC 1 ampere is defined as the current flowing through two parallel infinitely long wires, 1m apart, which produces 1N of force.
So there you go. Something I bet you didn't need to know.
More tangent
What a coincidence that we already have Void
as a unit!!
I believe every other unit is a based on those 5.
I’ve typically seen this listed as seven fundamental units, with the amp and the mole being the other two. (The old definition of the amp was 2x10^-7 N/m; force scales with length so infinitely long wires would have infinite total force.)
While multiplying different units would be tremendously useful, I can't see how the number of combinations could be catered for.
Without an ability to parametrise generic types with numbers it's not possible to do compile time checks as in C++, but other than that I don't see any issues with a number of combinations. A minimal illustrating example:
struct Unit {
// TODO: rational power coefficients
var m: Int8 = 0
var s: Int8 = 0
var kg: Int8 = 0
// ...
}
let length = Unit(m: 1, s: 0, kg: 0)
let time = Unit(m: 0, s: 1, kg: 0)
let mass = Unit(m: 0, s: 0, kg: 1)
let velocity = Unit(m: 1, s: -1, kg: 0)
let acceleration = Unit(m: 1, s: -2, kg: 0)
extension Unit {
static func + (lhs: Unit, rhs: Unit) -> Unit {
// check units compatibility and either trap or return NaN equivalent
lhs
}
static func * (lhs: Unit, rhs: Unit) -> Unit {
Unit(m: lhs.m + rhs.m, s: lhs.s + rhs.s, kg: lhs.kg + rhs.kg)
}
static func / (lhs: Unit, rhs: Unit) -> Unit {
Unit(m: lhs.m - rhs.m, s: lhs.s - rhs.s, kg: lhs.kg - rhs.kg)
}
}
let a = length / time // ok, velocity
let b = length + length // ok, length
let c = length + time // runtime error or NaN
I’ve typically seen this listed as seven fundamental units, with the amp and the mole being the other two. (The old definition of the amp was 2x10^-7 N/m; force scales with length so infinitely long wires would have infinite total force.)
Yes, the per metre bit was a careless omission. However the 2e-7 bit I was totally ignorant of.
Nevertheless, I feel the Amp being based on the "primitive 3" still holds, along with the mole as that's really just a scaling factor based on mass.