goldfirere / units

The home of the units Haskell package
94 stars 19 forks source link

Applicative/Functor instances for Qu #40

Closed jameysharp closed 9 years ago

jameysharp commented 9 years ago

Maybe this is a terrible idea; I'd appreciate feedback. I think I want instances of all the Functor-related classes for Qu d l. Some uses would be semantically strange but still useful; for instance, Qu Length l (Int -> Float) is a funny-looking type but I guess it's a function that, given a length as an Int, returns a (possibly different) length as a Float. Obviously most of the library can't operate on such a type, but you'd use it with Applicative's <*> as an intermediate value on the way to something that the library can operate on.

My immediate use cases are that:

  1. I want to read sensor values from hardware as, say, Int16; tag them with a unit describing how to convert from hardware ADC counts to an SI unit, but not change their representation yet; and only later when I actually need to compute on them, fmap fromIntegral to convert them to Float and then maybe pick a new LCSU.
  2. I'm using kmett's linear and ad packages, which involve structuring my matrices and systems of equations as data types which are instances of Functor and assorted other classes. I want to tag dimensionality on all the elements of each matrix, but I think writing my Functor etc. instances will require the corresponding classes for Qu. I could write them directly in terms of Data.Metrology.Unsafe but that would be messy.
goldfirere commented 9 years ago

I hate to say it, but this is a terrible idea: it breaks the unit abstraction. I'm actually totally fine with Qu Length l (Int -> Float). I'm not fine at all, however, with fmap (+5) some_quantity -- the (+5) has no units specified! This allows you to observe what units are used internally to store a quantity, something that should never be visible (ignoring rounding errors.... which were one of the motivating factors for LCSUs, of course).

We really want to allow only operations parametric in units to be fmapped. Something like fmap (>0) some_quantity is unit-safe. But fmap (>1) some_quantity is not. I don't think the type system is up to distinguishing these!

On the other hand, I would be happy with adding these instances to Data.Metrology.Unsafe if you want. I agree that they're useful -- they're just unsafe! Or, much better, I'd love to see a way to make these kinds of operations safe, but I haven't the foggiest clue of how to do so.

If you want a matrix of quantities, is there a way of using Data.Metrology.Vector to your advantage?

jameysharp commented 9 years ago

I had the feeling there was some safety issue but I couldn't quite see what it was. Thanks for the examples! That's why I filed an issue instead of a pull request...

I considered suggesting putting the instances in Data.Metrology.Unsafe but I thought it was too weird to deal with the instance export rules. I guess since Unsafe.hs isn't imported by any other module in the units package, it's OK to put the instances there because they won't be accidentally re-exported that way. Is that right?

It's possible to build safe interfaces on top of these. For instance, I'm generating C code using the Ivory EDSL, which has a SafeCast typeclass for type conversions that preserve value. A function like this is safe, I think, and if it weren't for the Fractional constraint you could tell it's safe from the type alone:

unitCast :: (SafeCast from to, ConvertibleLCSUs d l1 l2, Fractional to) => Qu d l1 from -> Qu d l2 to
unitCast = convert . fmap safeCast

Inconveniently, a module providing a safe interface like this would effectively re-export the unsafe Functor etc. instances. So maybe that's still the wrong plan.

I don't understand the vector-space library, but it's clearly missing a lot of functionality I need that's in linear. That said, writing the relevant instances for linear looks about equivalent to writing them for vector-space.

goldfirere commented 9 years ago

Perhaps it's possible to build a units interface on top of linear, instead of vector-space. I didn't consider doing this, so it's not as if I've preferred vector-space over linear -- it's just what I was familiar with at the time. It is sad that these interactions have to be "built in" the units library. Something openly compositional would be far better. But we're not there, somehow.

In answer to your questions: You bring up a good point about re-export of instances. Putting unsafe instances in Data.Metrology.Unsafe effectively prohibits a client from building a safe interface over Data.Metrology.Unsafe. Yuck! I'm open to Data.Metrology.UnsafeInstances as a separate module, just so that there's a canonical location for these instances, preventing possible orphan-instance conflicts down the road.

Here's an idea for how you can get your Functor instance while remaining safe, demonstrated easiest by example:

module MySafeInterface ( safeOperation ) where

import Data.Metrology.Unsafe

newtype WQu d l n = Wrap { unwrap :: Qu d l n }
instance Functor (WQu d l) where
  fmap f (Wrap (Qu x)) = Wrap (Qu (f x))

safeOperation :: (...) => Qu d l n1 -> Qu d l n2
safeOperation x = unwrap $ do_amazing_things_that_require_Functor $ Wrap x

If your safeOperation needs to take structures containing quantities, you can always use Data.Coerce.coerce to do the wrapping and unwrapping.

This is far from pretty, but it just might work. Do you think this would fit your use-case?

jameysharp commented 9 years ago

Oh hey, that might work! And what would you think of exporting something like WQu from Data.Metrology.Unsafe? I might suggest a less-wieldy name like UnsafeQu for emphasis though.

As long as Qu and UnsafeQu have the same kind (as they do in your example), I think I can parameterize my vector datatypes over whether the quantities are safe or not. Then at least it will be obvious from the types when I need to be careful.

I should probably have mentioned that the vector and matrix types in question are in some sensor fusion via Kalman filtering code that I'm working on. Those types are defined in estimator:Numeric.Estimator.Model.SensorFusion, in case you want to have a look.

I think it has turned out that getting units properly encoded in my types is too complicated for now (I haven't even mentioned some other aspects I think are nastier than these) so I will have to come back to this. But at least there's a plausible direction to try when I have some spare time. I don't mind if you'd prefer to close this issue until then. Thanks for your insights!

goldfirere commented 9 years ago

I'm going to leave this open as a suggestion to put UnsafeQu in Data.Metrology.Unsafe. And thank you!

goldfirere commented 9 years ago

@jameysharp may be interested in the new branch linear, as described in #45. I would be happy with a linear interface to units, but I don't have the expertise (or time, frankly) to build it out. (Truth be told, I don't think it would take a ton of time... but I don't have the time in which to gain the expertise!)