I recently read the very interesting paper Monotonicity Types in which a new HM-language is described that keeps track of monotonicity across operations, so that the programmer does not have to do this manually (and fail at compile-time when a non-monotonic operation is passed to something that requires one).
I was thinking that it probably would be possible to model this in Haskell, since the sfun
s that the paper describes seem to be 'just another Arrow instance', so I set out to create a very small POC.
However, I came across the problem that there are, simply put, four kinds of 'tonicities' (for lack of a better term): monotonic, antitonic, constant (which is both) and unknown (which is neither), which can turn into one-another under composition or application:
When two 'tonic functions' are applied, the resulting tonic function's tonicity ought to be the most specific one that matches both types ('Qualifier Contraction; Figure 7' in the paper):
- if both are constant tonicity, the result should be constant.
- if both are monotonic, the result should be monotonic
- if both are antitonic, the result should be antitonic
- if one is constant and the other monotonic, the result should be monotonic
- if one is constant and the other antitonic, the result should be antitonic
- if one is monotonic and one antitonic, the result should be unknown.
- if either is unknown, the result is unknown.
When two 'tonic functions' are composed, the resulting tonic function's tonicity might flip ('Qualifier Composition; Figure 6' in the paper):
- if both are constant tonicity, the result should be constant.
- if both are monotonic, the result should be monotonic
- if both are antitonic, the result should be monotonic
- if one is monotonic and one antitonic, the result should be antitonic.
- if either is unknown, the result is unknown.
I have a problem to properly express this (the relationship between tonicities, and how 'tonic functions' will compose) in Haskell's types. My latest attempt looks like this, using GADTs, Type Families, DataKinds and a slew of other type-level programming constructs:
{-# LANGUAGE GADTs, FlexibleInstances, MultiParamTypeClasses, AllowAmbiguousTypes, UndecidableInstances, KindSignatures, DataKinds, PolyKinds, TypeOperators, TypeFamilies #-}
module Main2 where
import qualified Control.Category
import Control.Category (Category, (>>>), (<<<))
import qualified Control.Arrow
import Control.Arrow (Arrow, (***), first)
main :: IO ()
main =
putStrLn "Hi!"
data Tonic t a b where
Tonic :: Tonicity t => (a -> b) -> Tonic t a b
Tonic2 :: (TCR t1 t2) ~ t3 => Tonic t1 a b -> Tonic t2 b c -> Tonic t3 a c
data Monotonic = Monotonic
data Antitonic = Antitonic
class Tonicity t
instance Tonicity Monotonic
instance Tonicity Antitonic
type family TCR (t1 :: k) (t2 :: k) :: k where
TCR Monotonic Antitonic = Antitonic
TCR Antitonic Monotonic = Antitonic
TCR t t = Monotonic
--- But now how to define instances for Control.Category and Control.Arrow?
I have the feeling I am greatly overcomplicating things. Another attempt I had introduced
class (Tonicity a, Tonicity b) => TonicCompose a b where
type TonicComposeResult a b :: *
but it is not possible to use TonicComposeResult
in the instance declaration of e.g. Control.Category
("illegal type synonym family application in instance").
What am I missing? How can this concept properly be expressed in type-safe code?
Tonic t :: * -> * -> *
has the right kind forArrow
orCategory
, butTonic Antitonic
is not a category since the composition of two (antitonic) arrows is no longer an (antitonic) arrow. Indeed, antitonic functions do not make a category. At best, we can make a category with "functions of known tonicity", using a type which does not expose the tonicity (a sort of existential type) and juggle betweenTonicity t a b
(not a category) andSomeTonicity a b
(category). Alternatively, we could use aIndexedCategory
class to allow for tonicity changes. – KeroIndexedCategory
class would work? – Burbleclass IC k where type F k t1 t2 ; type B k ; comp :: k t1 a b -> k t2 b c -> k (F k t1 t2) a c ; id :: k (B k) a c
, I guess, forcingk
to define the composition rule for indicest
s (and the index forid
). This is untested, it might require some more changes. – KeroKnownNat n
andSomeNat
? – BurbleKnownNat
, but the spirit is similar. This is assuming that you really want/need to reuse the standard arrow/category class, – KeroArrow
s were first introduced as generalization of Monads because there were instances of them whose type specifications did not completely match up. Is there a more generalArrow
-like interface waiting from the shadows that might be useful for cases like these, where the return type of composition might fill in a differentt
? – BurbleIC
above to arrows. Anyway, many standard typeclasses over the years have been extended/generalized in the libraries, but only a few of such generalizations proved useful enough to become widespread. This however does not mean that one should not experiment and have some fun ;) – Kerot
(but can also not compose in the same way because they are restricted categories?). (b) convert between a general and an existential type that does not exposet
, but then there is no way to convert back. (Is this true?). (c) Usage of an IndexedCategory. Exactly how this would solve the problem I don't know yet, because I'd think that would hit the same limits as my attempt above(?) (Please post an answer!) or (d) hand-roll a more general Category and Arrow. – Burble