2

I am implementing a varity of functions that require type safe natural numbers in Haskell, and have recently needed an Exponential type to represent a new type.

Below are the three type families that I have made up to this point for ease of reference.

type family Add n m where
  Add 'One n = 'Succ n
  Add ('Succ n) m = 'Succ (Add n m)

-- Multiplication, allowing ever more powerful operators
type family Mul n m where
  Mul 'One m = m
  Mul ('Succ n) m = Add m (Mul n m)

-- Exponentiation, allowing even even more powerful operators
type family Exp n m where
  Exp n 'One = n
  Exp n ('Succ m) = Mul n (Exp n m)

However, when using this type I came across the issue that it wasn't injective; this meant that some of the type inferences I kinda wanted didn't exist. (The error was NB: ‘Exp’ is a non-injective type family). I can ignore the issue by using -XAllowAmbiguousTypes, but I would prefer not to use this extension, so all the types can checked where the function is defined.

I think that Exp n m should be injective when m is constant, so I wanted to try to implement that, but I'm unsure as to how to do that after a lot of trial and error. Even if it doesn't solve my problem currently, it might be useful in future. Alternatively, Exp n m is injective for a given n where m changes, and n is not One.

Upon asking other people, they suggested that something like type family Exp n m = inj | inj, n -> m where but that doesn't work, giving a syntax error on the comma if it's there and a parse error on the final n if it isn't. This was intended to allow inj and n to uniquely identify a given m.

The function I am trying to implement but am having trouble with currently has a signature as follows.

tensorPower :: forall i a n m . (Num a, KnownNat i) => Matrix n m a -> Matrix (Exp n i) (Exp m i) a

This function can be called with tensorPower @Three a (when -XAllowAmbiguousTypes is set), but I'd like GHC to be able to determine the i value by itself if possible. For the purposes of this question it's fine to assume that a given a matrix isn't polymorphic.

Adjusting the constraint to the following doesn't work either; this was an attempt at creating injectivity in the type of the above function instead of where the type family is defined

forall i a n m
   . ( Num a
     , KnownNat i
     , Exp n ( 'Succ i) ~ Mul n (Exp n i)
     , Exp m ( 'Succ i) ~ Mul m (Exp m i)
     , Exp n One ~ n
     , Exp m One ~ m
     )

So, is it possible to implement injectivity for this function, and if so, how do I implement it?

(To see more of the code in action, please visit the repository. The src folder has the majority of the source of the code, with the main areas being fiddled with in this question belonging to Lib.hs and Quantum.hs. The extensions used can (mostly) be found in the package.yaml)

L0neGamer
  • 275
  • 3
  • 13
  • 1
    If `zero` is a polymorphic matrix, and I write `tensorPower zero :: Matrix 4 4 Int`, how should the compiler decide between `i ~ 2, n ~ 2, m ~ 2` and `i ~ 1, n ~ 4, m ~ 4`? – Daniel Wagner Mar 09 '21 at 17:37
  • 2
    I believe this is far beyond what GHC currently supports, even for the cases where injectivity holds. Even if we know `n,m` and `n ~ Exp m i` to find `i` we need to compute the logarithm. We can't reasonably expect GHC to know how to do that when we only provide the definition for `Exp`. Proving that something is injective is one thing, being able to compute the inverse function is another. – chi Mar 09 '21 at 17:43
  • @DanielWagner Thanks for the comment! I agree in the case that a polymorphic argument is given to `tensorPower` the compiler would be unable to decide what to do, but that's hardly a unique case with this function; would adjusting the question so that the argument has to have fully defined dimensions satisfy you? The intention is that the `i` can expand to fit the output to an external operator, such as a multiplication. – L0neGamer Mar 09 '21 at 20:33
  • Injectivity here is not trivial, and there is no way for the user to supply an injectivity proof to GHC. So you can't do this. Often, however, you can make such things work by using constraints that specify both a function and its (partial) inverse. – dfeuer Mar 09 '21 at 20:50
  • Use a class with Functional Dependencies, instead of a `type family`. See `AddNat` here: https://stackoverflow.com/questions/60585777/multi-way-fundeps-and-consistency-with-overlapping-instances-why-does-this-wo – AntC Mar 09 '21 at 20:54
  • @AntC I'm having trouble parsing that question; would you mind writing an answer demonstrating how it would work? – L0neGamer Mar 09 '21 at 21:15
  • The manual for Injective type families is clear: from the result alone you must be able to obtain the argument(s) https://downloads.haskell.org/~ghc/8.10.4/docs/html/users_guide/glasgow_exts.html#syntax-of-injectivity-annotation. Syntax like `inj n -> m` is from Functional Dependencies, see the Jones2000 paper linked from here https://downloads.haskell.org/~ghc/8.10.4/docs/html/users_guide/glasgow_exts.html#functional-dependencies – AntC Mar 10 '21 at 00:46
  • Why are you rewriting what is provided in `GHC.TypeNats`? – hololeap Mar 11 '21 at 21:31
  • 1
    @hololeap firstly, I wanted to understand how to do this myself, so I implemented it from scratch. Secondly, I wanted to have my Nat type start from One, so I didn't have to deal with 0-length vectors. Thirdly, I didn't know about that. – L0neGamer Mar 12 '21 at 11:18
  • @AntC I asked for a solution in answer form since that's kinda how SO works; if you're trying to provide a solution, please put an answer. – L0neGamer Mar 12 '21 at 11:20
  • @L0neGamer you should also check out the [`vector-sized`](https://hackage.haskell.org/package/vector-sized) package if you haven't seen it yet. – hololeap Mar 12 '21 at 18:33
  • @hololeap oo, thanks! this implementation seems a bit... well it seems lower level and a bit more complex than my one, but it's certainly a different way of doing it. thanks for the recommendation! – L0neGamer Mar 14 '21 at 12:49

1 Answers1

2

There's actually a surprisingly simple way to get this to work in at least one way; the following type family, when used appropriately in a constraint, allows tensorPower to be used without an annotation.

-- Reverse the exponent - if it can't match then it goes infinitely
type family RLog n m x c where
  RLog m n n i = i
  RLog m n x i = RLog m n (Mul m x) ('Succ i)

type ReverseLog n m = RLog n m n 'One
type GetExp n i = ReverseLog n (Exp n i)
----------------
-- adjusted constraint for tensorPower
forall i a n m . (Num a, KnownNat i, i ~ GetExp n i, i ~ GetExp m i)

For example, now one can type (tensorPower hadamard) *.* (zero .*. zero .*. one) (where hadamard is Matrix Two Two Double, both zero and one are Matrix Two One Double, (*.*) is matrix multiplication, (.*.) is the tensor product and the type of i is completely inferred).

The way this type family works is that it has four parameters: the base, the target, the accumulator, and the current exponent. If the target and the accumulator are equal, then the current exponent is "returned". If they are not equal, we recurse by multiplying the current accumulator by the base, and increment the current exponent.

There is one issue with this solution that I can see: if it can't match up the "bases", it has a really horribly long error message as it recurses as deep as it can into types. This can be fixed by doing some other type trickery which is out of scope for this question, but can be seen in this commit on my project's repository.

In conclusion: introducing some abstract injectivity seemed to be a no-go, but implementing a sort of reversal of the exponent resulted in clean, simple, and functioning code - this is effectively injectivity, by proving that there is a reversible function for the Exp.

(One note is that this solution needs a bit more fiddling to work fully, since GetExp n i doesn't really work for n=='One; I've gotten around this by never having GetExp ('One) i in the first place)

L0neGamer
  • 275
  • 3
  • 13