TL;DR: If we want to close the fixpoint at top level when calling new
, instead of closing fixpoint for each traits (which I presume will cause some problems with the operational semantics), due to the use of calling constructors from the self reference in implementing the doubleNum
trait, we might need to use bounded polymorphism (to enable virtual constructors) + structural unfolding rule (to allow type parameters in unfold annotations) to achieve the desired compositional typing.
TOC
- Compositional interfaces are the same:
- [[#implementing-the-evalnum-trait-is-usual-does-not-require-self-reference|Implementing the
evalNum
trait is usual, does not require self-reference]] - [[#implementing-doublenum-requires-self-reference---v1|Implementing
doubleNum
requires self-reference - v1]] - Composition and type checking
- [[#attempt-1---parameterizing-doublenum|Attempt 1 - parameterizing
doubleNum
]] - [[#attempt-2---parameterizing-doublenum—refining-with-intersection-types|Attempt 2 - parameterizing
doubleNum
+ refining with intersection types]]- [[#Attempt 2 - parameterizing
doubleNum
+ refining with intersection types#What can we do with the new typing? - does not help|What can we do with the new typing? - does not help]]
- [[#Attempt 2 - parameterizing
- Attempt 3 - How about bounded polymorphism + structural unfolding?
- [[#Attempt 3 - How about bounded polymorphism + structural unfolding?#Composition again: success?|Composition again: success?]]
After understanding the Trait compositions in CP, let’s refine the example presented in Encoding CP with distributive recursive types
Compositional interfaces are the same:
except that we are now adding a layer of mu
to every (compositional) method interface
Source Language:
type NumSig<Exp> = { Lit : Int -> Exp; Add : Exp -> Exp -> Exp };
type IEval = { eval : Int };
type IDouble = { double : Self };
-- ** elaborates to:
type NumSig<Exp> = { Lit : Int -> Exp; Add : Exp -> Exp -> Exp };
type IEval = mu X. { eval : Int };
type IDouble = mu X. { double : X };
type IEvalDouble = IEval & IDouble
-- eval and double can be implemented on their own
<: mu X. ({ eval : Int } & { double : X } )
type IEvalDouble' = mu X. ({ eval : Int } & { double : X } )
-- eval double needs to be mutually defined
-- so IEvalDouble is a subtype of IEvalDouble'
-- Implication: maybe it is a good idea to keep interfaces defined as
-- IEvalDouble instead of IEvalDouble'
-- until all the compositions are completed
-- (calling the `new` constrcutor)
Implementing the evalNum
trait is usual, does not require self-reference
evalNum (self: Top) : NumSig<IEval> = {
Lit (val: Int) = fold [IEval] { eval = val } ;
Add (e1 : IEval ) (e2 : IEval) = fold [IEval]
{ eval = (unfold [ IEval ] e1).eval + (unfold [ IEval ] e2).eval }
}
The only difference to current CP is that we add some dummy folds so the output type is a recursive type mu X. {eval : Int}
Implementing doubleNum
requires self-reference - v1
In order to call the constructor, we have to use self reference in the doubleNum
trait, so that we can call self.Lit
, self.Add
, …
doubleNum (self: NumSig<IDouble>) : NumSig<IDouble> = {
Lit (val: Int) = fold [IDouble] { double = self.Lit (val + val) } ;
Add (e1 : IDouble ) (e2 : IDouble) = fold [IDouble]
{ double = self.Add
(unfold [IDouble] e1).double
(unfold [IDouble] e2).double
}
-- Type checking `doubleNum`
-- { double = self.Lit (val + val) } : { double : IDouble }
-- fold [IDouble] { ..... } : IDouble
-- (unfold [IDouble] e1).double : IDouble
-- unfold [IDouble] e1 : { double : IDouble }
-- e1 : IDouble
-- if we write the trait type, doubleNum has the type
-- Trait[NumSig<IDouble>, NumSig<IDouble>]
-- FYI:
-- NumSig<IDouble> = {
-- Lit : Int -> mu X. { double : X }
-- Add : mu X. { double : X }
-- -> mu X. { double : X }
-- -> mu X. { double : X }
-- }
Composition and type checking
We wish to compose the two traits
doubleNum : Trait[NumSig<IDouble>,NumSig<IDouble>]
andevalNum : Trait[NumSig<IEval>
However, in order to do so, we have to first close the doubleNum
fixpoint into Trait[NumSig<IDouble>]
and then merge Trait[NumSig<IEval>] & Trait[NumSig<IDouble>]
in order to obtain the final type NumSig<IEvalDouble>
.
e1 : NumSig<IEvalDouble> =
(fix [NumSig<IEval>] evalNum) ,, (fix [NumSig<IDouble>] doubleNum)
Question: can we instead write:
(fix NumSig<IEvalDouble> (evalNum,,doubleNum))
?
We get
Trait[NumSig<IDouble>, NumSig<IEvalDouble>]
fromevalNum ,, doubleNum
, this is not an instantiable trait! (provided typeNumSig<IEvalDouble>
is not a subtype of the required typeNumSig<IDouble>
)
Not with the current traits! Because the input type of the
doubleNum
trait isNumSig<IDouble>
, and it is not a subtype/supertype ofNumSig<IEvalDouble>
. (due to the both covariant and contravariant occurrences ofExp
in theAdd
constructor)
Attempt 1 - parameterizing doubleNum
Can we parameterize the doubleNum
trait?
doubleNum[Exp] (self: NumSig<Exp>) : ? = {
Lit (val: Int) = fold [IDouble] { double = self.Lit (val + val) } ;
Add (e1 : IDouble ) (e2 : IDouble) = fold [IDouble]
{ double =
self.Add (unfold [IDouble] e1).double (unfold [IDouble] e2).double
} -- type error! because we get the parameterized `{ double : Exp }` type before applying the fold constructor, but the fold requires a concrete `IDouble = mu X. { double : X }` recursive type annotation
Attempt 2 - parameterizing doubleNum
+ refining with intersection types
Can we use intersection type to ensure that there is a IDouble
interface?
I think the best we can do to type doubleNum
is as follows:
doubleNum[Exp] (self: NumSig<IDouble, IDouble & Exp>)
: NumSig<IDouble & Exp, IDouble> = {
Lit (val: Int) = fold [IDouble] { double = self.Lit (val + val) } ;
Add (e1 : IDouble & Exp ) (e2 : IDouble & Exp ) = fold [IDouble]
{ double = self.Add
(unfold [IDouble] e1).double
(unfold [IDouble] e2).double
}
Justification for “best” typing:
Given that iso-recursive unfold/folds only allow concrete recursive types in the annotation, the typings for the following items are fixed (cannot be further parameterized/generalized ):
fold [IDouble] { double = self.Lit (val + val) } : IDouble
(unfold [IDouble] e1).double : IDouble
(unfold [IDouble] e2).double : IDouble
fold [IDouble] { ... } ( in the Add constructor) : IDouble
1. Therefore, for the output type of the Lit constructors, they have to be `IDouble`, not `Exp`.
2. Note that we are applying the self reference at `self.Add (unfold [IDouble] e1).double`, the input type of the `self.Add` constructor must be `IDouble` and nothing more else.
3. The output type of `self`-reference, or the input type of the constructors, can be relaxed to a parametrized type `IDouble & Exp`, so `Exp` can be later instantiated with more interfaces.
What can we do with the new typing? - does not help
e2 : NumSig<IEvalDouble> = new evalNum ,, doubleNum @ IEval
evalNum ,, doubleNum @ IEval has the type:
Trait[NumSig<IEval>]
& Trait[NumSig<IDouble, IEvalDouble>, NumSig<IEvalDouble, IDouble]
<: Trait[NumSig<IDouble, IEvalDouble>, NumSig<IEvalDouble, IEvalDouble>]
NumSig<IEvalDouble, IEvalDouble> <: NumSig<IDouble, IEvalDouble> ?
IDouble <: IEvalDouble ? No!
The expected subtyping does not hold, the composed trait it not instantiable.
Attempt 3 - How about bounded polymorphism + structural unfolding?
doubleNum[Exp <: IDouble] (self: NumSig<Exp>)
: NumSig<Exp, IDouble> = {
Lit (val: Int) = fold [IDouble] { double = self.Lit (val + val) } ;
Add (e1 : Exp ) (e2 : Exp ) = fold [IDouble]
{ double = self.Add (unfold [Exp] e1).double (unfold [Exp] e2).double }
Lit constructor has type { Lit : Int -> IDouble } because
{ double = self.Lit (val + val) } : { double : Exp } <: { double : IDouble}
Add constructor has type { Add : Exp -> Exp -> IDouble } because
1)
e1 : Exp
Exp <: IDouble = mu X. {double : X}
{double : Exp} = { double : X } [ X |-> Exp ]
--------------------------------------- struct-unfold
(unfold [Exp] e1) : { double : Exp }
---------------------------------------
(unfold [Exp] e1).double : Exp
2)
self.Add : Exp -> Exp -> Exp
3)
{ double = ... } : { double : Exp } <: { double : IDouble }
In this definition:
doubleNum has the type:
Forall(Exp <: IDouble).Trait[NumSig<Exp>, NumSig<Exp,IDouble>]
Composition again: success?
e3 : NumSig<IEvalDouble> = new evalNum ,, doubleNum @ (IEval & IDouble)
evalNum ,, doubleNum @ (IEval & IDouble) has the type:
Trait[NumSig<IEval>]
& Trait[NumSig<IEvalDouble>, NumSig<IEvalDouble, IDouble>]
<: Trait[NumSig<IEvalDouble>, NumSig<IEvalDouble, IEvalDouble>]
OK this merge is instantiable
However, for bounded quantification + distributivity subtyping, the metetheory is not clear (might be hard)
Recall that we only have restricted results for bounded quantification + intersection types (without distributivity) in JFP
Attempt 4 - structural rules + encoding F<: with intersection types
doubleNum[Exp] (self: NumSig<IDouble & Exp>)
: NumSig<IDouble & Exp, IDouble > = {
Lit (val: Int) = fold [IDouble] { double = self.Lit (val + val) } ;
Add (e1 : IDouble & Exp ) (e2 : IDouble & Exp ) = fold [IDouble]
{ double = self.Add
(unfold [IDouble & Exp] e1).double
(unfold [IDouble & Exp] e2).double
}
Type checking:
1)
e1 : IDouble & Exp
IDouble & Exp <: IDouble = mu X. {double : X}
{double : IDouble & Exp} = { double : X } [ X |-> IDouble & Exp ]
--------------------------------------- struct-unfold
(unfold [IDouble & Exp] e1) : { double : IDouble & Exp }
---------------------------------------
(unfold [IDouble & Exp] e1).double : IDouble & Exp
2)
self.Add : IDouble & Exp -> IDouble & Exp -> IDouble & Exp
3)
{ double = ... } : { double : IDouble & Exp } <: { double : IDouble }
4)
fold [IDouble] { double = ... } : IDouble
Note that the structural rule for folding is:
e : A [a |-> B ]
mu a. A <: B
----------------
fold [B] e : B
IDouble & Exp are not supertypes of IDouble, so we only get IDouble as the output
the final type of the trait is:
Forall(Exp).Trait[NumSig<IDouble & Exp>, NumSig<IDouble & Exp, IDouble>]
e : X
mu a. A & X <: mu a. B
--------------- structural unfolding
unfold [A] e : B [X/a]
e : mu a. A
--------------- structural unfolding
unfold [mu a. A] e : A [A/a]
so
evalNum ,, doubleNum @ (IEval) has the type:
Trait[NumSig<IEval>]
& Trait[NumSig<IEvalDouble>, NumSig<IEvalDouble, IDouble>]
<: Trait[NumSig<IEvalDouble>, NumSig<IEvalDouble>]