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?
The universe of tonicities is fixed, so a single data type would be more accurate. The data constructors are lifted to the type level with the DataKinds
extension.
data Tonicity = Monotone | Antitone | Constant | Unknown
Then, I would use a newtype to represent tonic functions:
newtype Sfun (t :: Tonicity) a b = UnsafeMkSfun { applySfun :: a -> b }
To ensure safety, the constructor must be hidden by default. But users of such a Haskell EDSL would most likely want to wrap their own functions in it. Tagging the name of the constructor with "unsafe" is a nice compromise to enable that use case.
Function composition literally behaves as function composition, with some extra type-level information.
composeSfun :: Sfun t1 b c -> Sfun t2 a b -> Sfun (ComposeTonicity t1 t2) a c
composeSfun (UnsafeMkSfun f) (UnsafeMkSfun g) = UnsafeMkSfun (f . g)
-- Composition of tonicity annotations
type family ComposeTonicity (t1 :: Tonicity) (t2 :: Tonicity) :: Tonicity where
ComposeTonicity Monotone Monotone = Monotone
ComposeTonicity Monotone Antitone = Antitone
...
ComposeTonicity _ _ = Unknown -- Any case we forget is Unknown by default.
-- In a way, that's some extra safety.
来源:https://stackoverflow.com/questions/56001863/how-to-work-with-types-that-change-under-composition