Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add functions to convert between different forms of the same quantity type #32

Open
ianmackenzie opened this issue Apr 23, 2019 · 7 comments

Comments

@ianmackenzie
Copy link
Owner

All just no-ops that just convert between equivalent units types:

  • Force.energyPerLength : Quantity Float (Rate Joules Meters) -> Force
  • Force.asEnergyPerLength : Force -> Quantity Float (Rate Joules Meters)
  • Energy.massTimesSpeedSquared : Quantity Float (Product Kilograms (Squared MetersPerSecondSquared)) -> Energy
  • Energy.asMassTimesSpeedSquared : Energy -> Quantity Float (Product Kilograms (Squared MetersPerSecondSquared))

Possible breaking change: perhaps then switch back to Force, Energy etc. having their own units types, e.g.

type Newtons
    = Newtons

instead of

type alias Newtons =
    Product Kilograms MetersPerSecondSquared

and then always convert to/from rates/products where needed. More symmetric (doesn't 'bless' one particular representation as the canonical one), and perhaps less confusing - always need to cast to a rate/product if you need one. In this case we would also need

  • Force.massTimesAcceleration : Quantity Float (Product Kilograms MetersPerSecondSquared) -> Force
  • Force.asMassTimesAcceleration : Force -> Quantity Float (Product Kilograms MetersPerSecondSquared)
  • Energy.forceTimesLength : Quantity Float (Product Newtons Meters) -> Energy
  • Energy.asForceTimesLength : Energy -> Quantity Float (Product Newtons Meters)
@ianmackenzie
Copy link
Owner Author

Non-breaking version: keep units the same (one canonical units type per quantity type), but for uniformity support functions like Force.massTimesAcceleration even though

Force.massTimesAcceleration mass acceleration

could be written as

mass |> Quantity.times acceleration

Then also provide Force.asMassTimesAcceleration as a no-op. This means that code could be written using massTimesAcceleration, asEnergyPerLength etc. without having to worry about what the actual underlying units are.

Call these functions "constructors" and "reinterpretations"?

@ianmackenzie
Copy link
Owner Author

For the 'constructors', should they be of the form

Speed.lengthPerDuration : Quantity Float (Rate Meters Seconds) -> Speed

-- e.g.
Speed.lengthPerDuration (Length.kilometers 100 |> Quantity.per (Duration.hours 1))

or

Speed.lengthPerDuration : Length -> Duration -> Speed

-- e.g.
Speed.lengthPerDuration (Length.kilometers 100) (Duration.hours 1)

?

The latter is more convenient for simple cases but is asymmetric with

Speed.asLengthPerDuration : Speed -> Quantity Float (Rate Meters Seconds)

and is less flexible since it forces the caller to pass separate values (which could be annoying if they get a Rate quantity returned by some function and want to work with it directly). Maybe have both versions:

Speed.lengthPerDuration : Length -> Duration -> Speed
Speed.fromLengthPerDuration : Quantity Float (Rate Meters Seconds) -> Speed
Speed.toLengthPerDuration : Speed -> Quantity Float (Rate Meters Seconds)

@edgerunner
Copy link

edgerunner commented May 19, 2020

I have been playing around with some functions that practically do nothing to the value other than change its signature. I think it may be useful to define some operations that transform the complex signatures (combinations of Product and Rate) into their equivalents.

lift :
    Area
    -> Quantity Float Unitless
    -> Density
    -> Speed
    -> Force
lift wingArea liftCoefficent airDensity speed =
    squared speed
        -- ((L/T)(L/T))
        |> normalizeRR
        -- ((LL)/(TT))
        |> times airDensity
        -- (((LL)/(TT))(M/((LL)L))
        |> normalizeRR
        -- (((LL)M)/((TT)((LL)L)))
        |> onRate flip flip
        -- ( (M(LL)) / ( ((LL)L)(TT) ) )
        |> onRate identity shiftRight
        -- ( (M(LL)) / ( (LL)(L(TT)) ) )
        |> simplifyPP
        -- ( M / (L(TT)) )
        |> times wingArea
        -- (( M / (L(TT)) )(LL))
        |> normalizeRN
        -- ((M(LL))/(L(TT)))
        |> onRate shiftLeft identity
        -- (((ML)L)/(L(TT)))
        |> simplifyPP
        -- ((ML)/(TT))
        |> extractFirst
        -- (M(L/(TT)))
        |> onProduct identity extractLast
        -- (M((L/T)T))
        |> apply liftCoefficent
        |> half

normalizeRR :
    Quantity number (Product (Rate a b) (Rate c d))
    -> Quantity number (Rate (Product a c) (Product b d))
normalizeRR =
    repackage


normalizeRN :
    Quantity number (Product (Rate a b) c)
    -> Quantity number (Rate (Product a c) b)
normalizeRN =
    repackage


simplifyPP :
    Quantity number (Rate (Product a b) (Product b c))
    -> Quantity number (Rate a c)
simplifyPP =
    repackage


onRate :
    (Quantity number a -> Quantity number c)
    -> (Quantity number b -> Quantity number d)
    -> Quantity number (Rate a b)
    -> Quantity number (Rate c d)
onRate _ _ =
    repackage


onProduct :
    (Quantity number a -> Quantity number c)
    -> (Quantity number b -> Quantity number d)
    -> Quantity number (Product a b)
    -> Quantity number (Product c d)
onProduct _ _ =
    repackage


flip :
    Quantity number (Product a b)
    -> Quantity number (Product b a)
flip =
    repackage


shiftRight :
    Quantity number (Product (Product a b) c)
    -> Quantity number (Product a (Product b c))
shiftRight =
    repackage


shiftLeft :
    Quantity number (Product a (Product b c))
    -> Quantity number (Product (Product a b) c)
shiftLeft =
    repackage


extractFirst :
    Quantity number (Rate (Product a b) c)
    -> Quantity number (Product a (Rate b c))
extractFirst =
    repackage


extractLast :
    Quantity number (Rate a (Product b c))
    -> Quantity number (Rate (Rate a b) c)
extractLast =
    repackage


repackage : Quantity number a -> Quantity number b
repackage (Quantity value) =
    Quantity value

-- multiplies a unit with a `Unitless` value without altering the original signature
apply : Quantity Float Unitless -> Quantity number unit -> Quantity number unit
apply (Quantity value) =
    Quantity.multiplyBy value

the tough part is actually figuring out and covering all the required atomic operations, as well as finding good names for them (a classic)

On the other hand, this is too much noise for the signal.

@ianmackenzie
Copy link
Owner Author

@edgerunner sounds like you're doing some pretty sophisticated stuff with elm-units, very cool! My first reaction is that a function like lift is a good candidate for writing like

lift : Area -> Quantity Float Unitless -> Density -> Speed -> Force
lift (Quantity wingArea) (Quantity liftCoefficent) (Quantity airDensity) (Quantity speed) =
    Quantity (0.5 * speed ^ 2 * airDensity * wingArea * liftCoefficient)

as discussed here. Yes, you do have to be careful to make sure that the expression inside the lift function is correct - if you leave off one of the factors then the compiler cannot help you catch that error - but as long as that's correct then the use of that function is just as type-safe as anything else.

The original plan for elm-units was actually to have no mathematical functions at all, so that you'd have to unwrap/re-wrap to do any math on Quantity values. I was eventually convinced that it would be useful to have a suite of at least common math operations, but I think the main benefit of elm-units is still in making contracts between modules/packages more explicit (e.g. a function that accepts a Length is much harder to accidentally use improperly than once that accepts a Float). The ability to have type-checked mathematical expressions is certainly nice, and I do take advantage of it myself, but I think it's a secondary benefit.

That said, if there are some additional kinds of multiplication/division operations that we can come up with good names for, then I'm open to adding those.

One final note - you could write

apply factor quantity

as

Quantity.multiplyBy (Quantity.toFloat factor) quantity

if you wanted to avoid an extra helper function - I think that's the only one of those functions that could be rewritten in terms of existing Quantity functions.

@ianmackenzie
Copy link
Owner Author

Another potentially interesting approach could be to have a single repackage function (I think I'd be inclined to call it coerce) and then have an elm-review rule that somehow checked whether the coercion was valid (by doing a proper dimensionality check). I'm not sure elm-review is actually capable of that level of sophisticated type checking, but if so that could be pretty cool! This would be similar to the approach taken by the yaiouom Rust crate.

@edgerunner
Copy link

At one point I was thinking of coercing the base unit types into a single union type which also includes combinators Rate and Product. That could be reduced into a single multi-dimension tuple/record.

I gave that idea up because there was no way of converting that back to one of the elm-units types without either giving up the guarantee or making that a Maybe. That defeats the purpose for me.

That approach could be used in something like elm-review. I'd be up for writing the dimensionality check, do you think it would be useful?

@ianmackenzie
Copy link
Owner Author

ianmackenzie commented Jun 15, 2023

Oops please ignore that last comment (now deleted), I meant to comment on #62. That'll teach me to try to do GitHub work on my phone 😅

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants