Goals for today:
We've seen a few cases where one might want to write a formula without using specific simple types.
In the simple type system, there is no way to express a general identity function. The best you can do is write one at each type:
However, the intuition of what we want is very clear. Untyped (or as Carpenter 1998 calls it, "monotyped"):
With a type variable:
I have claimed that the core of a standard linguistic compositional semantics amounts to the following combinators (plus lexical lookup). Here annotated with type variables:
Where $X$ and $Y$ are any(?) type.
Equative claims:
Maybe: [[is]] = $\lambda x_X . \lambda y_X . x = y$
"And" is not very picky about the apparent semantic types of what it combines:
A lot to say about the details of how to do this, but long story short, type flexibility ($\langle X,\langle X,X \rangle \rangle$) is one way.
We want to try adding some sort of type variable.
How to make sense of: $\lambda x_{X} . x$ etc?
First things first: Lambda notebook types can be directly constructed using a built in function, tp
(type parser).
x = tp("<e,t>")
x
tp("<X,Y>")
Types are represented by objects that subclass lamb.types.TypeConstructor
. Like metalanguage objects, they have a python interface, e.g..
x = tp("<e,t>")
display(x[0], x.functional())
True
lamb.meta.get_type_system().unify(tp("<e,t>"), tp("X"))
Complex types need a "type constructor".
FunType
, arity 2), sets (SetType
, arity 1), tuples (TupleType
, arity $\geq 0$).(For a bit more on this in a linguistic contex, see: https://rawlins.io/various/bracketed_e_is_not_a_type.pdf)
Recap:
Type inference: given two types $a,b$, what (if any) single type $c$ is equivalent to $a$ and $b$?
In the simply-typed lambda calculus, type inference is the same thing as type checking. If $a$ and $b$ are equal, they can be unified as $a$ (or $b$), otherwise, they cannot be unified.
unify(tp("<e,t>"), tp("<e,t>"))
unify(tp("<e,t>"), tp("e")) # returns None
unify(tp("t"), tp("e")) # returns None
If two types $\alpha$ and $\beta$ contain type variables, unification is harder (and more interesting).
unify(tp("X"), tp("<e,t>"))
unify(tp("<X,t>"), tp("<e,Y>"))
unify(tp("<X,t>"), tp("<Y,Z>"))
unify(tp("<X,<Y,e>>"), tp("<Y,<Z,X>>"))
unify(tp("<X,t>"), tp("<e,X>"))
No solution to this one: the two instances of $X$ are the same variable.
There is a convenient shorthand for guaranteeing free type variables:
unify(tp("<?,t>"), tp("<e,?>"))
"Fancy" type systems are well-studied in computer science. I will focus on two here. First, one that is overly powerful:
Explicit representation for type variable binding. Example, identity function again:
$id = \Lambda X . \lambda x_X . x : \forall X . X \rightarrow X$
Reduction example:
We will go simpler...
Milner and Damas (1982), "Principle Type Schemes for Functional Programs". Hindley (1969), "The Principal Type-Scheme of an Object in Combinatory Logic".
Restriction of System F: $\forall$ can never be embedded under other type operators.
Begin with an empty assignment. For two types $\alpha, \beta$:
If $\alpha$ and $\beta$ are functions, recursively unify $\alpha_0, \beta_0$ relative to the assignment, and $\alpha_1, \beta_1$ relative to the assignment.
The end result is a modified type and an assignment of principle types to all variables in $\alpha$ and $\beta$.
What would happen if we ran the above algorithm on $\langle X, X \rangle$ and $X$?
unify(tp("<X,X>"), tp("X"))
ERROR (types): Failed occurs check: can't unify recursive types <X,X> and X
Milner and Damas introduced Algorithm W, which is an efficient unification algorithm for this system. See Damas' PhD thesis for more details.
Haskell's type system, approximately: HM + type annotations for non-decidable parts of system F + other stuff.
You may be wondering - what is python?
Example: functions.
__call__
.->
notation, that encapsulates this idea.)def test(x):
return x
test.__call__
<method-wrapper '__call__' of function object at 0x119539560>
class FunClass:
def __call__(self, x):
return x
test2 = FunClass()
test2.__call__
<bound method FunClass.__call__ of <__main__.FunClass object at 0x119854b10>>
test2(1)
1
Yesterday, I sketch a natural and simple algorithm for doing type checking for function-argument combinations.
f.type.functional()
and f.type[0] == a.type
More abstract (and powerful) approach:
f.type
and $\langle$ a.type
$, X \rangle$ be unified? (Where $X$ is a "fresh" type variable.)If types change, any corresponding metalanguage object needs to change as well.
f = %te L x_X : x
f.try_adjust_type(tp("<e,e>")).derivation
f2 = %te L f_<X,Y> : L x_X : f(x)
f2.try_adjust_type(tp("<<e,Y>,<e,Y>>")).derivation
Implementation sketch:
None
, fail.Note that because formula construction already involves type checking, the result is guaranteed to be consistent.
Implementing tree-based computation, and top-down/deferred computation.
*
.reload_lamb()
Tree = lamb.utils.get_tree_class()
lang.set_system(lang.hk3_system)
lang.hk3_system
%%lamb
||gray|| = L x_e : Gray_<e,t>(x)
||cat|| = L x_e : Cat_<e,t>(x)
||Joanna|| = J_e
||is|| = L f_<e,t> : f
||a|| = L f_<e,t> : f
Warning: variable name 'is' is reserved and will be shadowed in python
t = Tree("S", ["NP", "VP"])
r = lang.hk3_system.compose(t)
r.tree()
r.paths()
t2 = Tree.fromstring("(NP (gray) (N (cat)))")
t2
# syntax here is still pretty cumbersome
r2 = lang.get_system().expand_all(lang.CompositionTree.tree_factory(t2))
r2.paths()
t3 = Tree.fromstring("(S (DP (Joanna)) (VP (V (is)) (DP (a) (NP (gray) (N (cat))))))")
t3
r3 = lang.get_system().compose(lang.CompositionTree.tree_factory(t3))
r3
r3 = lang.get_system().expand_all(lang.CompositionTree.tree_factory(t3))
r3
r3.paths()