Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to work with types that change under composition?

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?

like image 990
Qqwy Avatar asked May 06 '19 08:05

Qqwy


People also ask

When should you use inheritance over composition?

To get the higher design flexibility, the design principle says that composition should be favored over inheritance. Inheritance should only be used when subclass 'is a' superclass. Don't use inheritance to get code reuse. If there is no 'is a' relationship, then use composition for code reuse.

How do you decide between inheritance and composition?

The main difference between inheritance and composition is in the relationship between objects. Inheritance: “is a.” E.g. The car is a vehicle. Composition: “has a.” E.g. The car has a steering wheel.

Can we achieve polymorphism through composition?

No, not really. Polymorphism and composition or aggregation (composition is a more rigid form of aggregation wherein the composed objects' lifetimes are tied together) are different ways of reusing classes. Composition involves aggregating multiple objects to form a single entity.

What are the two most common reasons to use inheritance?

[1] Many people use classical inheritance to achieve polymorphism instead of letting their classes implement an interface. The purpose of inheritance is code reuse, not polymorphism. Furthermore, some people use inheritance to model their intuitive understanding of an "is-a" relationship which can often be problematic.


1 Answers

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.
like image 125
Li-yao Xia Avatar answered Nov 06 '22 22:11

Li-yao Xia