ekmett / linear

Low-dimensional linear algebra primitives for Haskell.
http://hackage.haskell.org/package/linear
Other
201 stars 50 forks source link

Add monomorphic classes for * alongside existing ones for * → * #17

Open liyang opened 11 years ago

liyang commented 11 years ago

Current classes in linear are only instantiable for * → * kinds. We'd be able to supplant vector-space had we classes for * kinds as well.

Preliminary discussion on #haskell-lens; @acowley @ekmett @shachaf

dmcclean commented 10 years ago

This is an interesting proposal/discussion.

Could monomorphic classes coexist with polymorphic classes? Either with the monomorphic ones superclasses of the polymorphic ones or with an opt-in once-and-for-all (potentially-)overlapping instance of the monomorphic ones in terms of the polymorphic ones?

Having the coexisting lerp and affine space operators is confusing in modules that need both.

My use for monomorphic classes isn't performance when specialized to Double, it's types that represent various notions of time. Although now that I am thinking about it "out loud" I wonder if I can just rearrange my time types to be polymorphic and then use type synonyms to hide it in places that don't need to be concerned with it...., so I'm going to go investigate that.

dmcclean commented 10 years ago

Hmm. Almost.

The sticking point is the Functor superclass requirement on Additive. I can't make dimensional-dk's Quantity d (where d is a phantom type representing a physical dimension), which I wish to make Additive, an instance of Functor because if I did people could fmap under the units and do nonsense things, mostly defeating the point.

After browsing the source (of just the Linear.Vector module, not of the whole package, so this may be the tip of the iceberg) it looks like the reason for this constraint it twofold. First it is used to define negation and subtraction, second it's very convenient for lots of other things that would otherwise need a more verbose constraint.

In contrast the Data.AdditiveGroup module has the negation function inside the typeclass and doesn't define a default implementation. (It also has the subtraction function defined in the obvious way in terms of negation, but outside the typeclass.)

bergey commented 10 years ago

If diagrams were to use linear, we'd have a similar problem to @dmcclean's. Our Angle type should hide whether it is represented by radians, degrees, or something else, and fmap (with suitably chosen function) does too much.

It's probably OK to trust users not to do nonsense like fmap (\x -> x*x) (180 degrees), but I'd be interested in seeing a design for monomorphic Additive for this sort of thing. I can't promise that I have time to work on it, though.

More discussion: http://ircbrowse.net/browse/diagrams?events_page=1512

ekmett commented 10 years ago

The main hazard I see with 'monomorphic Additive' is what does it mean to multiply a matrix in that world?

We have matrix multiplication as:

(!*!) :: ... => m (t a) -> t (n a) -> m (n a)

Every proposal I've seen has led to situations where you have some vector spaces that can only occur on the 'right hand side' of a matrix.

That is many things, but its not a vector space. ;)

dmcclean commented 10 years ago

One thing I was thinking about the other day was that I ran into a type like:

data Atmos a = Atmos { atmosTemperature :: a
, atmosPressure :: a
, atmosDensity :: a
, atmosSpeedOfSound :: a
, atmosViscosity :: a
, atmosKinematicViscosity :: a
}

Where you could have a Functor instance. But when you get to the dimensional version of it:

data Atmos a = Atmos { atmosTemperature :: ThermodynamicTemperature a
, atmosPressure :: Pressure a
, atmosDensity :: Density a
, atmosSpeedOfSound :: Velocity a
, atmosViscosity :: DynamicViscosity a
, atmosKinematicViscosity :: KinematicViscosity a
}

you run into a different version of the same thing. And thus I couldn't match the Functor g context in jacobian :: (Traversable f, Functor g, Num a) => (forall s. Reifies s Tape => f (Reverse s a) -> g (Reverse s a)) -> f a -> g (f a).

So maybe what's actually wanted isn't monomorphic vector spaces but weaker functors. Functors that only can fmap functions that are in some vague sense "identityish" in that they change the representation of a number but don't change what number it is.

Since this is (a) (hopelessly?) vague, (b) undecidable, and (c) in a package depended on by the entire universe, it seems like the best thing to do is probably to stop worrying and love the bomb.

Or maybe this, which limits the scope in which you are willing to love the bomb:

newtype UnsafeQuantity d a = UnsafeQuantity (Quantity d a)

instance Functor (UnsafeQuantity d) where
  fmap = -- unwrap it, trust the supplied function, and wrap it again
bergey commented 10 years ago

I agree - if I don't make my types Functor, I'm going to need to define something with a different name but the same behavior, to use realToFrac inside the type. So I'm convinced to "love the bomb" as you say.

dmcclean commented 10 years ago

Thinking about it again, there's a way you can look at it that makes this problem the same as the dreaded "restricted monad"/"how do I make Data.Set a monad" question.

Adopting the Functor version of the ConstraintKinds solution to the restricted monad problem:

class RFunctor f where
  type RFunctorCtxt f a :: Constraint
  fmap :: (RFunctorCtxt f a, RFunctorCtxt f b) => (a -> b) -> f a -> f b

You could have

instance RFunctor (Quantity d) where
  type RFunctorCtxt (Quantity d) a = (Real a)
  fmap _ (Quantity x) = Quantity (fromRational $ toRational x)

Even though its implementation of fmap completely ignores the function being mapped, it still respects the functor laws (assuming the Real instance(s) is/are well behaved).

This formalizes what I meant by "functions that are in some vague sense "identityish" in that they change the representation of a number but don't change what number it is" and ensures that we only fmap those functions.

Not worth doing, but interesting to think about.

ekmett commented 10 years ago

This has come up as a proposed solution before, but "restricted functors" get in the way of building things like jets of derivatives using linear, and you lose the fact that the second functor law follows from the first via parametricity, etc. the cure is worse than the disease IMHO.

dmcclean commented 10 years ago

There's little doubt that it's way worse than the disease, that's for sure.

Especially because I can't see how to recover the ordinary Functor in terms of RFunctor. You need a way to say that RFunctorCtxt f a ~ () that is universally quantified over a, and I don't think we have a weapon that does that, so you can't write a constraint synonym like type Functor f = forall a.(RFunctor f, RFunctorCtxt f a ~ ())

I don't understand the comment about jets of derivatives, but it's probably because I don't understand what a jet is.

ekmett commented 10 years ago

The way I formulate it in AD:

data Jet f a = a :- Jet f (f a)

which looks something like

a :- f a :- f (f a) :- f (f (f a)) :- ...
ekmett commented 10 years ago

It is basically an 'unzipped cofree comonad'.

You can go from Cofree f a to Jet f a for any functor f, but going back isn't always possible (except for representable functors).

dmcclean commented 10 years ago

That's cool. So the idea is that whatever f is structuring the results of your function, you can still build higher order derivatives because you can keep replicating the structure.

Which you couldn't do with restricted functors, basically for the reason that you can't express the constraint that a restricted functor is "ordinary" (in the sense that the context it's restricted to is always ()).

ekmett commented 10 years ago

Technically its the f that was structuring the inputs to your function. you wind up (basically) computing

grads :: (f AD -> AD) -> Jet f Double
jacobians :: (f AD -> g AD) -> g (Jet f Double)

When I figured that out was a bit of an 'aha!' moment for me, and it maps back onto the limit of the notion of an n-jet of derivatives in mathematics as n goes to infinity.

dmcclean commented 10 years ago

I think I see.

So you actually need an even weaker property. You don't need a restricted functor that has context () for all types, you need one where (RFunctorCtxt f a) entails (RFunctorCtxt f (f a)), which is even less encodable into GHC-Haskell.

ekmett commented 10 years ago

Yes, but that is just one particular polymorphically recursive case. I have EDSLs that happen to use others. I basically view the 'restricted monad encoding' as a bad abstraction because it leaks all information about what intermediate steps you pass through into your types and renders things that were once canonical into merely one path through the minefield of unusable types.

dmcclean commented 10 years ago

Indeed. It's weird because these rock/hard-place situations are pretty uncommon in haskell. It seems like you either have to (a) leak the constraint and propagate them everywhere or (b) leak the representation you are using for your units and lose scale invariance. It doesn't even seem like having a more refined typeclass hierarchy than the Num situation would help, either.

Strangely this is one situation where I actually miss having escape hatches from parametricity. In C# land I would think about adding the fmap equivalent and then use reflection to inspect the types I am actually being asked to fmap between and either construct the appropriate conversion function or raise an exception saying that there isn't on. This gives the static appearance that everything is fine, but can't actually be used to build "unsafe" programs that don't explode the first time they are run; you are kept up at night by the possibility that someone only breaks the rules in some extremely unusual scenario that won't be caught in testing, but doing that by accident is not at all easy.

Interesting discussion. I'm going with the newtype approach that limits the availability of the leaky Functor instance, I think.