Types, type inference, and polymorphism¶

NASSLLI 2022: implementing semantic compositionality¶

Kyle Rawlins, kgr@jhu.edu¶

Johns Hopkins University, Department of Cognitive Science¶

Goals for today:

  • Look at type inference in detail
  • Better understanding of type variables/polymorphism and their implementation
  • More examples

Some motivation: type variables¶

We've seen a few cases where one might want to write a formula without using specific simple types.

Case 1: identity functions¶

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:

  • $\lambda x_e . x$, $\lambda x_t . x$
  • $\lambda x_{\langle e, t \rangle} x$, $\lambda x_{\langle e, e \rangle} x$, $\lambda x_{\langle t, t \rangle} x$, $\lambda x_{\langle t, e \rangle} x$
  • ...

However, the intuition of what we want is very clear. Untyped (or as Carpenter 1998 calls it, "monotyped"):

  • $\lambda x . x$

With a type variable:

  • $\lambda x_X . x$, where $X$ is any(?) type.

Case 2: combinators more generally¶

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:

  1. FA: $\lambda f_{\langle X,Y \rangle} . \lambda x_X . f(x)$
  2. NN: $\lambda x_{X} . x$
  3. GPM: $\lambda f_{\langle X,t \rangle} . \lambda g_{\langle X,t \rangle} . \lambda x_X . f(x) \wedge g(x)$

Where $X$ and $Y$ are any(?) type.

Case 3: flexible equatives¶

Equative claims:

  1. Cicero is Tully.
  2. The start of the day is the end of the night.
  3. What John believes is what Mary believes.
  4. To be is to be perceived.

Maybe: [[is]] = $\lambda x_X . \lambda y_X . x = y$

Case 4: flexible "and"¶

"And" is not very picky about the apparent semantic types of what it combines:

  1. Alfonso read P&P and S&S. (type $e$)
  2. Alfonso read P&P and liked S&S. (type $\langle e,t \rangle$)
  3. Alfonso read and liked P&P. (type $\langle e,\langle e,t \rangle \rangle$)
  4. Alfonso read P&P and he liked S&S. (type $t$)
  5. ...

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.

  • Partee & Rooth 1983, "Generalized conjunction and type ambiguity" + much following work.
  • CCG syntactic category for "and": $(X\backslash X)/X$ (see Steedman 2001, The Syntactic Process, for an overview)

So: type variables¶

We want to try adding some sort of type variable.

How to make sense of: $\lambda x_{X} . x$ etc?

  • Intuition: it's a bit like having a meta-level $\lambda$ operator that can be saturated by a more specific type.

Implementing types¶

First things first: Lambda notebook types can be directly constructed using a built in function, tp (type parser).

In [84]:
x = tp("<e,t>")
x
Out[84]:
$\left\langle{}e,t\right\rangle{}$
In [93]:
tp("<X,Y>")
Out[93]:
$\left\langle{}X,Y\right\rangle{}$

Type objects and type constructors¶

Types are represented by objects that subclass lamb.types.TypeConstructor. Like metalanguage objects, they have a python interface, e.g..

In [100]:
x = tp("<e,t>")
display(x[0], x.functional())
$e$
True
In [99]:
lamb.meta.get_type_system().unify(tp("<e,t>"), tp("X"))
Out[99]:
$\left\langle{}e,t\right\rangle{}$

Complex types need a "type constructor".

  • Intuition: construct a complex type from one or more parts and associate it with particular metalanguage behaviors
  • Type constructors for functions (FunType, arity 2), sets (SetType, arity 1), tuples (TupleType, arity $\geq 0$).
  • Type implementation is not independent of metalanguage.

(For a bit more on this in a linguistic contex, see: https://rawlins.io/various/bracketed_e_is_not_a_type.pdf)

Simple types and type checking¶

Recap:

Type inference: given two types $a,b$, what (if any) single type $c$ is equivalent to $a$ and $b$?

  • Combining $a$ and $b$ in this way is sometimes referred to as type unification.
  • Call $c$ a principle type.

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.

In [105]:
unify(tp("<e,t>"), tp("<e,t>"))
Out[105]:
$\left\langle{}e,t\right\rangle{}$
In [106]:
unify(tp("<e,t>"), tp("e")) # returns None
In [107]:
unify(tp("t"), tp("e")) # returns None

Towards polymorphic unification¶

If two types $\alpha$ and $\beta$ contain type variables, unification is harder (and more interesting).

In [110]:
unify(tp("X"), tp("<e,t>"))
Out[110]:
$\left\langle{}e,t\right\rangle{}$
In [112]:
unify(tp("<X,t>"), tp("<e,Y>"))
Out[112]:
$\left\langle{}e,t\right\rangle{}$
In [114]:
unify(tp("<X,t>"), tp("<Y,Z>"))
Out[114]:
$\left\langle{}X,t\right\rangle{}$
In [109]:
unify(tp("<X,<Y,e>>"), tp("<Y,<Z,X>>"))
Out[109]:
$\left\langle{}e,\left\langle{}e,e\right\rangle{}\right\rangle{}$
  • Three (consistent) equalities: $X = e$, $X = Y$, $Y = Z$.
  • There is no solution that does not involve specifying the variables.
  • $e$ is the principle type for each of these type variables.
In [115]:
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:

In [116]:
unify(tp("<?,t>"), tp("<e,?>"))
Out[116]:
$\left\langle{}e,t\right\rangle{}$

Polymorphic types¶

"Fancy" type systems are well-studied in computer science. I will focus on two here. First, one that is overly powerful:

System F (Girard, Reynolds)¶

Explicit representation for type variable binding. Example, identity function again:

$id = \Lambda X . \lambda x_X . x : \forall X . X \rightarrow X$

Reduction example:

  • $id(e)(y : e) $
  • $ = (\Lambda X . \lambda x_X . x : \forall X . X \rightarrow X)(e)(y: e)$
  • $= (\lambda x : e . x : e \rightarrow e)(y : e)$
  • $= y : e$
  • System F is (surprisingly) strongly normalizing.
  • With "type annotations", type checking is decidable.
  • Haskell: uses a version of system F.

We will go simpler...

Damas-Hindley-Milner (in brief)¶

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.

  • E.g. no functions of type $(\forall X . X \rightarrow X) \rightarrow e$.
  • However, $\forall X . (X \rightarrow X) \rightarrow e$ is possible.
  • Let rule: way of "embedding" parametric polymorphism. $\mathbf{Let \: f=... in ...f...}$
  • Let scoping is automatically applied to any lexical entry.

A basic algorithm for type unification¶

Begin with an empty assignment. For two types $\alpha, \beta$:

  1. if $\alpha = \beta$, return.
  2. if $\alpha$ and $\beta$ are monotypes and not equal, fail.
  3. if $\alpha$ is a type variable: replace all instances of $\alpha$ with $\beta$ in the type and in the assignment. Add to the assignment: $\alpha \rightarrow \beta$.
  4. If $\beta$ is a type variable: replace all instances of $\beta$ with $\alpha$ in the type and in the assignment (right-hand-side). Add to the assignment: $\beta \rightarrow \alpha$.
  5. 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$.

Occurs checks¶

What would happen if we ran the above algorithm on $\langle X, X \rangle$ and $X$?

  • Infinite recursion.
  • Occurs check: if one side is a variable, does that variable occur embedded in the other side?
In [117]:
unify(tp("<X,X>"), tp("X"))
ERROR (types): Failed occurs check: can't unify recursive types <X,X> and X

Algorithm W¶

Milner and Damas introduced Algorithm W, which is an efficient unification algorithm for this system. See Damas' PhD thesis for more details.

  • I won't try to present it here. In the scale of type inference algorithms it is simple, but it isn't simple in absolute terms.

Duck typing?¶

Haskell's type system, approximately: HM + type annotations for non-decidable parts of system F + other stuff.

You may be wondering - what is python?

  • Modern Python allows for type annotations and there exist static type checkers, but it's an auxiliary thing.
  • Python's type system involves "duck typing". If it looks like a duck, acts like a duck, ..., then it is a duck.

Example: functions.

  • There is no type per se for a "function".
  • A python object can act like a function just in case it implements __call__.
  • (There is a type annotation, using -> notation, that encapsulates this idea.)
In [119]:
def test(x):
    return x

test.__call__
Out[119]:
<method-wrapper '__call__' of function object at 0x119539560>
In [121]:
class FunClass:
    def __call__(self, x):
        return x

test2 = FunClass()
test2.__call__
Out[121]:
<bound method FunClass.__call__ of <__main__.FunClass object at 0x119854b10>>
In [123]:
test2(1)
Out[123]:
1

Generalizing function-arg unification¶

Yesterday, I sketch a natural and simple algorithm for doing type checking for function-argument combinations.

  • Nutshell: f.type.functional() and f.type[0] == a.type

More abstract (and powerful) approach:

  • Can f.type and $\langle$ a.type $, X \rangle$ be unified? (Where $X$ is a "fresh" type variable.)

Type unification and metalanguage unification¶

If types change, any corresponding metalanguage object needs to change as well.

In [133]:
f = %te L x_X : x
f.try_adjust_type(tp("<e,e>")).derivation
Out[133]:
1.
$\lambda{} x_{X} \: . \: {x}$
2.
$\lambda{} x_{e} \: . \: {x}$
Type adjustment
In [136]:
f2 = %te L f_<X,Y> : L x_X : f(x)
f2.try_adjust_type(tp("<<e,Y>,<e,Y>>")).derivation
Out[136]:
1.
$\lambda{} f_{\left\langle{}X,Y\right\rangle{}} \: . \: \lambda{} x_{X} \: . \: {f}({x})$
2.
$\lambda{} f_{\left\langle{}X,Y\right\rangle{}} \: . \: \lambda{} x_{X} \: . \: {f}({x})$
Let substitution
3.
$\lambda{} f_{\left\langle{}e,Y\right\rangle{}} \: . \: \lambda{} x_{e} \: . \: {f}({x})$
Type adjustment

Implementation sketch:

  1. Run unification on the types, and get the resulting principle type mapping. If None, fail.
  2. Recurse into the formula doing all term substitutions implied by the principle type mapping. (Simple in concept, hard in practice.)

Note that because formula construction already involves type checking, the result is guaranteed to be consistent.

Composition in tree structures¶

Implementing tree-based computation, and top-down/deferred computation.

  • So far: we have only seen bottom-up composition using *.
  • But, the input to linguistic semantic compositional systems is more typically a tree of some kind.
In [47]:
reload_lamb()
Tree = lamb.utils.get_tree_class()
lang.set_system(lang.hk3_system)
lang.hk3_system
Out[47]:
Composition system 'H&K Tree version'
Operations: {
    Tree composition rule FA/left, built on python function 'lamb.lang.tree_left_fa_fun'
    Tree composition rule FA/right, built on python function 'lamb.lang.tree_right_fa_fun'
    Tree composition rule PM, built on python function 'lamb.lang.tree_pm_fun'
    Tree composition rule PA, built on python function 'lamb.lang.tree_pa_sbc_fun'
    Tree composition rule NN, built on python function 'lamb.lang.tree_nn_fun'
    Tree composition rule IDX, built on python function 'lamb.lang.tree_percolate_index'
    Tree composition rule VAC, built on python function 'lamb.lang.tree_binary_vacuous'
    Lexicon lookup
}
In [48]:
%%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
$[\![\mathbf{\text{gray}}]\!]^{}_{\left\langle{}e,t\right\rangle{}} \:=\: $$\lambda{} x_{e} \: . \: {Gray}({x})$
$[\![\mathbf{\text{cat}}]\!]^{}_{\left\langle{}e,t\right\rangle{}} \:=\: $$\lambda{} x_{e} \: . \: {Cat}({x})$
$[\![\mathbf{\text{Joanna}}]\!]^{}_{e} \:=\: $${J}_{e}$
$[\![\mathbf{\text{is}}]\!]^{}_{\left\langle{}\left\langle{}e,t\right\rangle{},\left\langle{}e,t\right\rangle{}\right\rangle{}} \:=\: $$\lambda{} f_{\left\langle{}e,t\right\rangle{}} \: . \: {f}$
$[\![\mathbf{\text{a}}]\!]^{}_{\left\langle{}\left\langle{}e,t\right\rangle{},\left\langle{}e,t\right\rangle{}\right\rangle{}} \:=\: $$\lambda{} f_{\left\langle{}e,t\right\rangle{}} \: . \: {f}$
In [128]:
t = Tree("S", ["NP", "VP"])
r = lang.hk3_system.compose(t)
r.tree()
Out[128]:
$[\![\mathbf{\text{NP}}]\!]^{}_{?}$
*
$[\![\mathbf{\text{VP}}]\!]^{}_{?}$
$[\![\mathbf{\text{S}}]\!]$
[path 0]:
$[\![\mathbf{\text{S}}]\!]^{}_{X'} \:=\: $$[\![\mathbf{\text{NP}}]\!]^{}_{\left\langle{}X,X'\right\rangle{}}([\![\mathbf{\text{VP}}]\!]^{}_{X})$
[path 1]:
$[\![\mathbf{\text{S}}]\!]^{}_{X'} \:=\: $$[\![\mathbf{\text{VP}}]\!]^{}_{\left\langle{}X,X'\right\rangle{}}([\![\mathbf{\text{NP}}]\!]^{}_{X})$
[path 2]:
$[\![\mathbf{\text{S}}]\!]^{}_{\left\langle{}e,t\right\rangle{}} \:=\: $$\lambda{} x_{e} \: . \: ([\![\mathbf{\text{NP}}]\!]^{}_{\left\langle{}e,t\right\rangle{}}({x}) \wedge{} [\![\mathbf{\text{VP}}]\!]^{}_{\left\langle{}e,t\right\rangle{}}({x}))$
In [127]:
r.paths()
Out[127]:
3 composition paths:
Path [0]:
$[\![\mathbf{\text{NP}}]\!]^{}_{?}$
*
$[\![\mathbf{\text{VP}}]\!]^{}_{?}$
[
FA/left
]
$[\![\mathbf{\text{S}}]\!]^{}_{X'}$
$[\![\mathbf{\text{NP}}]\!]^{}_{\left\langle{}X,X'\right\rangle{}}([\![\mathbf{\text{VP}}]\!]^{}_{X})$


Path [1]:
$[\![\mathbf{\text{NP}}]\!]^{}_{?}$
*
$[\![\mathbf{\text{VP}}]\!]^{}_{?}$
[
FA/right
]
$[\![\mathbf{\text{S}}]\!]^{}_{X'}$
$[\![\mathbf{\text{VP}}]\!]^{}_{\left\langle{}X,X'\right\rangle{}}([\![\mathbf{\text{NP}}]\!]^{}_{X})$


Path [2]:
$[\![\mathbf{\text{NP}}]\!]^{}_{?}$
*
$[\![\mathbf{\text{VP}}]\!]^{}_{?}$
[
PM
]
$[\![\mathbf{\text{S}}]\!]^{}_{\left\langle{}e,t\right\rangle{}}$
$\lambda{} x_{e} \: . \: ([\![\mathbf{\text{NP}}]\!]^{}_{\left\langle{}e,t\right\rangle{}}({x}) \wedge{} [\![\mathbf{\text{VP}}]\!]^{}_{\left\langle{}e,t\right\rangle{}}({x}))$


In [62]:
t2 = Tree.fromstring("(NP (gray) (N (cat)))")
t2
Out[62]:
NPgrayNcat
In [124]:
# syntax here is still pretty cumbersome
r2 = lang.get_system().expand_all(lang.CompositionTree.tree_factory(t2))
r2.paths()
Out[124]:
1 composition path:
$[\![\mathbf{\text{gray}}]\!]^{}_{\left\langle{}e,t\right\rangle{}}$
[
Lexicon
]
$\lambda{} x_{e} \: . \: {Gray}({x})$
*
$[\![\mathbf{\text{cat}}]\!]^{}_{\left\langle{}e,t\right\rangle{}}$
[
Lexicon
]
$\lambda{} x_{e} \: . \: {Cat}({x})$
[
NN
]
$[\![\mathbf{\text{N}}]\!]^{}_{\left\langle{}e,t\right\rangle{}}$
$\lambda{} x_{e} \: . \: {Cat}({x})$
[
PM
]
$[\![\mathbf{\text{NP}}]\!]^{}_{\left\langle{}e,t\right\rangle{}}$
$\lambda{} x_{e} \: . \: ({Gray}({x}) \wedge{} {Cat}({x}))$


In [65]:
t3 = Tree.fromstring("(S (DP (Joanna)) (VP (V (is)) (DP (a) (NP (gray) (N (cat))))))")
t3
Out[65]:
SDPJoannaVPVisDPaNPgrayNcat
In [68]:
r3 = lang.get_system().compose(lang.CompositionTree.tree_factory(t3))
r3
Out[68]:
3 composition paths. Results:
    [0]: $[\![\mathbf{\text{S}}]\!]^{}_{X'} \:=\: $$[\![\mathbf{\text{DP}}]\!]^{}_{\left\langle{}X,X'\right\rangle{}}([\![\mathbf{\text{VP}}]\!]^{}_{X})$
    [1]: $[\![\mathbf{\text{S}}]\!]^{}_{X'} \:=\: $$[\![\mathbf{\text{VP}}]\!]^{}_{\left\langle{}X,X'\right\rangle{}}([\![\mathbf{\text{DP}}]\!]^{}_{X})$
    [2]: $[\![\mathbf{\text{S}}]\!]^{}_{\left\langle{}e,t\right\rangle{}} \:=\: $$\lambda{} x_{e} \: . \: ([\![\mathbf{\text{DP}}]\!]^{}_{\left\langle{}e,t\right\rangle{}}({x}) \wedge{} [\![\mathbf{\text{VP}}]\!]^{}_{\left\langle{}e,t\right\rangle{}}({x}))$
In [69]:
r3 = lang.get_system().expand_all(lang.CompositionTree.tree_factory(t3))
r3
Out[69]:
1 composition path. Result:
    [0]: $[\![\mathbf{\text{S}}]\!]^{}_{t} \:=\: $$({Gray}({J}_{e}) \wedge{} {Cat}({J}_{e}))$
In [70]:
r3.paths()
Out[70]:
1 composition path:
$[\![\mathbf{\text{Joanna}}]\!]^{}_{e}$
[
Lexicon
]
${J}_{e}$
[
NN
]
$[\![\mathbf{\text{DP}}]\!]^{}_{e}$
${J}_{e}$
*
$[\![\mathbf{\text{is}}]\!]^{}_{\left\langle{}\left\langle{}e,t\right\rangle{},\left\langle{}e,t\right\rangle{}\right\rangle{}}$
[
Lexicon
]
$\lambda{} f_{\left\langle{}e,t\right\rangle{}} \: . \: {f}$
[
NN
]
$[\![\mathbf{\text{V}}]\!]^{}_{\left\langle{}\left\langle{}e,t\right\rangle{},\left\langle{}e,t\right\rangle{}\right\rangle{}}$
$\lambda{} f_{\left\langle{}e,t\right\rangle{}} \: . \: {f}$
*
$[\![\mathbf{\text{a}}]\!]^{}_{\left\langle{}\left\langle{}e,t\right\rangle{},\left\langle{}e,t\right\rangle{}\right\rangle{}}$
[
Lexicon
]
$\lambda{} f_{\left\langle{}e,t\right\rangle{}} \: . \: {f}$
*
$[\![\mathbf{\text{gray}}]\!]^{}_{\left\langle{}e,t\right\rangle{}}$
[
Lexicon
]
$\lambda{} x_{e} \: . \: {Gray}({x})$
*
$[\![\mathbf{\text{cat}}]\!]^{}_{\left\langle{}e,t\right\rangle{}}$
[
Lexicon
]
$\lambda{} x_{e} \: . \: {Cat}({x})$
[
NN
]
$[\![\mathbf{\text{N}}]\!]^{}_{\left\langle{}e,t\right\rangle{}}$
$\lambda{} x_{e} \: . \: {Cat}({x})$
[
PM
]
$[\![\mathbf{\text{NP}}]\!]^{}_{\left\langle{}e,t\right\rangle{}}$
$\lambda{} x_{e} \: . \: ({Gray}({x}) \wedge{} {Cat}({x}))$
[
FA/left
]
$[\![\mathbf{\text{DP}}]\!]^{}_{\left\langle{}e,t\right\rangle{}}$
$\lambda{} x_{e} \: . \: ({Gray}({x}) \wedge{} {Cat}({x}))$
[
FA/left
]
$[\![\mathbf{\text{VP}}]\!]^{}_{\left\langle{}e,t\right\rangle{}}$
$\lambda{} x_{e} \: . \: ({Gray}({x}) \wedge{} {Cat}({x}))$
[
FA/right
]
$[\![\mathbf{\text{S}}]\!]^{}_{t}$
$({Gray}({J}_{e}) \wedge{} {Cat}({J}_{e}))$


Day 4: recap¶

  1. Motivating type variables
  2. Basics of type implementation
  3. Polymorphic types generally
  4. The Damas-Handley-Milner system
  5. Another use case: composition in tree structures
  6. More examples, time permitting
In [ ]: