-
Notifications
You must be signed in to change notification settings - Fork 3
Implicit Arguments
This page is about the redesign of Agda's implicit argument facilities.
-
Implicit arguments can be given both by position and by name, but even an argument given by name must be in the right position.
-
Infix and mixfix operators do not take implicit arguments.
-
Module parameters can be explicit, but then it is unclear how to position the arguments of infix and mixfix operators (See Agda issue 632). This suggest to only allow implicit (= named) module parameters.
- For Agda it is not clear when to insert the hidden argument or abstraction.
-
In some cases, type checking only succeeds if a hidden lambda is eagerly inserted:
data Vec (A : Set) : Nat -> Set where vnil : Vec A zero vcons : {n : Nat} -> A -> Vec A n -> Vec A (suc n) Cons = {A : Set} (a : A) {n : Nat} -> Vec A n -> Vec A (suc n) cons : Cons cons a = vcons a
For check of
vcons a : {n : Nat} -> Vec A n -> Vec A (suc n)
to succeed, a hidden abstraction needs to be inserted:\{n} -> vcons {n = _} a
will solve the_
byn
.A more detailed account of type checking: First, the lhs is checked, building up a context of bound variables. The application
cons a
is internally expanded tocons {A} a
, as the presence of visible argumenta
forces insertion of prior hidden arguments, here,A
. After doing the minimum work on the lhs, the context isA : Set, a : A
. The remaining type is{n : Nat} -> Vec A n -> Vec A (suc n)
. A this point, there is a choice to make:-
Insert the hidden argument
n : Nat
on the lhs, leading tocons {A} a {n}
and contextA : Set, a : A, n : Nat
. Continuing checking the rhs against remaining typeVec A n -> Vec A (suc n)
, we switch to inference mode, as the rhs is an applicationvcons a
. First, the typevcons : {A : Set} {n : Nat} -> A -> Vec A n -> Vec A (suc n)
is inferred, then an application to visible argumenta
is encountered. At this point, meta variables are inserted for the hidden arguments tovcons
, leading tovcons {_A A a n} {_n A a n} a : Vec _A _n -> Vec _A (suc _n)
. Note that the freshly created metas can depend on (and only on) the bound variables in the current context, which isA
,a
, andn
. Finally, the inferred typeVec _A _n -> Vec _A (suc _n)
is unified with the expected typeVec A n -> Vec A (suc n)
leading to the solutions_A = A
and_n = n
. -
Do not insert hidden argument
n : Nat
but proceed directly to check the rhs. Type checking proceeds similar as in the first case, but the context is now onlyA : Set, a : A
which means that both created metas_A
and_n
can only depend on bound variablesA
anda
. Also, the final unification problem (inferred type <= expected type) isVec _A _n -> Vec _A (suc _n) <= {n : Nat} -> Vec A n -> Vec A (suc n)
. This problem is unsolvable, even with strong subtyping rules, asn
is not allowed to appear in the solution of_n
.
-
-
In other cases, type checking only succeeds if a hidden lambda is not inserted:
Ty = {T : Bool -> Set} -> ({b : Bool} -> T b -> T false) -> T true postulate f : Ty test : Ty test g = f g
After checking the lhs, we have context
T : Bool -> Set, g : {b : Bool} -> T b -> T false
, and we proceed to check the rhsf g
against remaining typeT true
. As we are dealing with an application, we switch to inference mode, gettingf : {T : Bool -> Set} -> ({b : Bool} -> T b -> T false) -> T true
and seeing an application to visible argumentg
. Now, a new meta_T
(which can depend onT
andg
) is inserted afterf
for the hidden argumentf
expects before visible argumentg
. The type off {_T}
is({b : Bool} -> _T b -> _T false) -> _T true
and we need to make sure thatg
is an acceptable argument to this function. When checkingg : {b : Bool} -> _T b -> _T false
, we are faced with a choice:-
Insert an implicit abstraction
\{b} -> g
which gets us to checkingg : _T b -> _T false
in contextT : Bool -> Set, g : {b : Bool} -> T b -> T false, b : Bool
. It remains to check that the inferred type ofg
is more general (or rather, not more specific) than the expected type, leading to the unification problem{b : Bool} -> T b -> T false <= _T b -> _T false
. These types do not have the same shape, but we could have inserted a hidden argument_b
afterg
(with meta variable_b
depending onT
,g
, andb
), in which case the unification problem becameT _b -> T false <= _T b -> _T false
. This problem decomposes intoT false <= _T false
and_T b <= T _b
. As we are using higher-order unification, there is no unique solution for_T
, and type checking fails. -
Do not insert an implicit abstraction, but check
g
directly against{b : Bool} -> _T b -> _T false
. The inferred type ofg
is{b : Bool} -> T b -> T false
, leading to the subtyping problem (inferred type <= expected type){b : Bool} -> T b -> T false <= {b : Bool} -> _T b -> _T false
. The difference to the first case is that nowb
is a bound variable on both sides, so we get the subproblems_T b <= T b
andT false <= _T false
. Subtyping is the same as equality (except maybe for implicit quantification such as{b : Bool} -> ...
where we could use the strong subtyping rules of Curry-style System F, see below); thus, we get the equation_T b = T b
and can solve_T = \ b -> T b = T
.
-
-
Goal: design implicit argument syntax that
- has a more predictable behavior
- still offers sufficient convenience to the user.
Behaviors we want (to preserve):
In the following example, the implicit argument j
should be tied to the projection tail
, i.e., whenever the projection is present, the implicit argument should be inserted eagerly.
record Stream {i : Size} (A : Set) : Set where
coinductive
constructor _∷_
field
head : A
tail : {j : Size< i} → Stream {j} A
open Stream public
map : ∀ {i A B} (f : A → B) (s : Stream {i} A) → Stream {i} B
head (map f s) = f (head s)
tail (map f s) = map f (tail s)
This will be expanded to:
map : ∀ {i A B} (f : A → B) (s : Stream {i} A) → Stream {i} B
head (map {i} {A} {B} f s) = f (head s)
tail (map {i} {A} {B} f s) {j} = map {j} {A} {B} f (tail s {j})
For reasons of termination, the implicit j : Size< i
must be bound on the left hand side. The result of lazy insertion
tail (map {i} {A} {B} f s) = \{j} -> map {j} {A} {B} f (tail s {j})
is not accepted by the termination checker. Currently, there is a preprocessing phase in the termination checker that moves the \{j}
to the left hand side. Maybe such extra effort can be avoided with the a new design.
- Implicit arguments should always be given by name.
- The order of implicit arguments should not matter that much. At least if implicit arguments belong to the same group, the order should be irrelevant. What a group is, is up to definition...
As the name of a named argument matters when giving it to a function, it makes sense it also matters in the type.
postulate
P : Nat → Set
T = {a : Nat}(A : P a) -> P a
T' = {b : Nat}(B : P b) -> P b
In this philosophy, the types T
and T'
are actually different. The following two definitions should be rejected:
T≡T' : T ≡ T'
T≡T' = refl
badcast : T → T'
badcast g = g
T
and T'
are only isomorphic, we can cast with a little more eta-expansion:
cast : T → T'
cast g {b = n} x = g {a = _} x
cast' : T' → T
cast' g x = g x -- works, since implicits are inserted when x is given
In essence, an implicit argument is like a record with one field: the name of the field matters.
We need to avoid the strong subtyping for polymorphic lambda-calculus, which is:
B[X/x] <= C
----------------- X fresh meta
{x : A} -> B <= C
y : C |- A <= D
-----------------
A <= {y : C} -> D
These rules are the source of undecidability of Curry-style System-F subtyping. In our setting, they create additional unification problems which might not be solvable.
We restrict to the structural subtyping which does not create new unification problems:
C <= A x:C |- B <= D
----------------------------
{x : A} -> B <= {x : C} -> D
The strong subtyping behavior can only be obtained with explicit insertion of lambdas or arguments:
e : {x : A} -> B
-----------------
e {x = X} : B[X/x] B[X/x] <= C
---------------------------------
e {x = X} : C
and
e : A x:C |- A <= D
---------------------------- ----------------------------
\{x = x} -> e : {x : C} -> A {x : C} -> A <= {x : C} -> D
------------------------------------------------------------
\{x = x} -> e : {x : C} -> D
We gain something from this stricter equality checking for types: We can allow permutation of implicit arguments. The relative order in which they are given should not matter (just like for records). For instance the following two types become equal:
T = {A : Set} {n : Nat} (v : Vec A n) -> (a : A) -> Vec A (suc n)
T' = {n : Nat} {A : Set} (v : Vec A n) -> (a : A) -> Vec A (suc n)
And we can give a named argument out of order.
f : T -> {A : Set} (v : Vec A 1) -> A -> Vec A 2
f snoc = snoc {n = 1}
A group is a telescope concerning at least one explicit argument. Groups are separated by ->
in function type. The implicits of a group are inserted when the explicit argument is given.
Telescopes without explicit arguments can also appear in function types, but these implicits are eagerly inserted. Such groups of implicits are only allowed in type signatures, but never in types that are computed.
Legal:
Type : {a : Level} -> Set (suc a)
Type {a = a} = Set a
Illegal:
T = {a : Level} -> Set (suc a)
Type : T
Type {a = a} = Set a
This is illegal already, but for a different reason, because one cannot name a level-polymorphic type (it lives in Set\omega, for which there is no expression).
Reason to rule this out (see issue 1095):
T : Nat -> Set
T zero = Nat
T (suc n) = {x : Nat} -> T n
Checking e : T _n
we do not know whether we have to insert a hidden lambda.
With groups, the definition of Ty
above is illegal, you cannot have a group of implicits only in a defined type. A legal version would be:
Ty = {T : Bool -> Set} (g : {b : Bool} (_ : T b) -> T false) -> T true
postulate f : Ty
test : Ty
test g = f g
Here, test g
will get the type T true
where g
has type {b : Bool}(_ : T b) -> T false
. We check the rhs f g
against T true
. Definitionf
infers to Ty
, no implicits inserted. When applying to g
we insert implicit _T
and check g
against {b : Bool}(_ : _T b) -> _T false
. This type unifies with the type of g
, yielding solution _T = T
. The type of f g
is inferred to be T true
.
Consider above example with interpreting ->
as group separator:
data Vec (A : Set) : Nat -> Set where
vnil : Vec A zero
vcons : {n : Nat} -> A -> Vec A n -> Vec A (suc n)
cons : {A : Set} (a : A) {n : Nat} -> Vec A n -> Vec A (suc n)
cons a = vcons a
This expands to
cons {A = A} a {n = n} = vcons {n = _} a
which will solve the _
by n
.
If we group the arguments to cons
differently, type-checking fails.
cons : {A : Set} (a : A) -> {n : Nat} (v : Vec A n) -> Vec A (suc n)
cons a = vcons a
Now, this expands to
cons {A = A} a = vcons {n = _} a
with no solution for _
.
As the argument order of vcons
matters, this is probably the best grouping, with index n
in the same group as xs
, the first argument referring to it.
data Vec (A : Set) : Nat -> Set where
vnil : Vec A zero
vcons : (x : A) -> {n : Nat} (xs : Vec A n) -> Vec A (suc n)
This minimizes the scope of the implicit parameter n
.
Do we need grouping for modules telescopes? See issue 682: mixfix printing destroyed by explicit module parameters.
An approach is to disallow explicit arguments in module telescopes. All arguments have to be given by name. With implicit arguments only, we do not get into the trouble that all holes in a mixfix operator are shifted, but it would be nice to have named arguments to mixfix ops as well. Needs extension of parser.
module M {A : Set} where
postulate
if_then_else_ : Bool -> A -> A -> A
open M -- gives: if_then_else_ : {A : Set} -> Bool -> A -> A -> A
test : Nat
test = if {A = Nat} true then 3 else 5
Fundamental problem: projections. Agda, as Haskell, confuses field and projection names. The dot-notation r.field
of OO and OCaml etc. would be a better choice for projecting a field.
No action plan yet: Maybe infix/mixfix ops in record modules should be forbidden.
Agda meeting XX, October 2014:
- Reuse Agda syntax
- Distinguish type schemes from types
Introductions (base types)
I ::= s -- sort (like Set l)
| D vs -- data type
| R vs -- record type
-- type operators, functions
| λ v -- abstraction
-- non-types
| c vs -- constructed thing
| l -- level
Types
v,A ::= I -- base type / introduction
| Π {Ss} Π S λS -- visible function type
-- eliminations: these are never turning into eager implicit
| X es -- meta variable
| x es -- variable
| f es -- function
Type Schemes
S ::= A
| Π {Ss} λI -- proper scheme: eagerly inserted implicits
Type signatures (refer to type schemes)
f : S
Types are closed under substitution as type variables stand for types only, not for type schemes. Type meta variables are also denoting type schemes.
Do we need meta variables for type schemes as well?
f : _
f = t