How to work with types that change under composition?

亡梦爱人 提交于 2019-12-10 04:00:00

问题


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 sfuns 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?


回答1:


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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!