Goal for today: see how a "composition system" is implemented in the lambda notebook, and how it can both used and extended.
"Frege's Conjecture": semantic composition is function application.
Perhaps the most influential presentation of a compositional system in linguistics (p. 43-44):
Arithmeticese: object language with one verb ("is"), two coordinators ("plus", "times"), various numbers ("two", "four", ...).
(1) Two plus two is four
# this is one way to write a "curried" function in python:
def plus(x):
return lambda y: y + x
def isV(x): # `is` is a python reserved word
return lambda y: x == y
def two():
return 2
def four():
return 4
plus(two())(four())
6
isV(plus(two())(two()))(four())
True
Weirdly adequate along a number of dimensions. But?
isV(two())(plus)
False
plus(isV(two())(two()))(four())
5
plus(two())
<function __main__.plus.<locals>.<lambda>(y)>
plus(isV)(two()) # finally, an error
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) /var/folders/wg/hnp9tqsn313f9dst_84rmsw80000gn/T/ipykernel_27872/4159620212.py in <module> ----> 1 plus(isV)(two()) # finally, an error /var/folders/wg/hnp9tqsn313f9dst_84rmsw80000gn/T/ipykernel_27872/1325836745.py in <lambda>(y) 1 # this is one way to write a "curried" function in python: 2 def plus(x): ----> 3 return lambda y: y + x 4 5 def isV(x): # `is` is a python reserved word TypeError: unsupported operand type(s) for +: 'int' and 'function'
If you're a python hacker, you might come up with something like this as a solution to 1:
lexicon = {"two": two(),
"four": four(),
"plus": plus,
"is": isV}
lexicon["plus"](lexicon["two"])(lexicon["two"])
4
Even better, we can abstract the notion of "composition" into a function:
def compose(f, a, lex):
# try looking up f and a in the lexicon, otherwise take them as given
if f in lex:
f = lex[f]
if a in lex:
a = lex[a]
return f(a)
compose(compose("plus", "two", lexicon), "two", lexicon)
4
This is essentially what we are aiming at!
But, we need a real solution to 1-4:
Abstract these ideas into general functions and classes.
Worth noting that other people doing this work often have picked Haskell exactly because it is arguably a better fit for the task than python.
Flip-side: it is not "mainstream" or especially easy to learn. It is not a good first language. It isn't widely in demand in industry jobs. It is not used for introductory computing courses.
Function Application: given two linguistic objects, one which is a function $f$ and the other an appropriate argument $a$, the result of composition is $f(a)$.
In a nutshell: all the work is in the metalanguage, in particular, getting a good implementation of beta reduction.
Standard(ish) setup:
If $f$ has type $\langle \alpha, \beta \rangle$, then $f$ is a function from things of type $\alpha$ to things of type $\beta$.
(After Heim and Kratzer 1998, Coppock and Champollion's 2022 Invitation to Formal Semantics ch. 6)
If $\gamma$ is a syntax tree whose subtrees are $\alpha$ and $\beta$, where:
Then, $[[\gamma]] = [[\alpha]]([[\beta]])$
%%lamb
||two|| = 2
||plus|| = L x_n : L y_n : x + y
plus * two # let's switch to notebook and play around with this a bit
(plus * two).trace()
For a * b
:
a.content
is a function, and b.content
is an appropriate argument, then:[[a b]] = a.content(b.content)
b.content
is a function, and a.content
is an appropriate argument, then:[[a b]] = b.content(a.content)
plus * plus
The lambda notebook metalanguage is implemented via structured python objects that all share a common programmatic inferface.
The %te
line magic (mnemonic: typed expression) lets you quickly build metalanguage objects:
formula = %te p_t & q_t
display(formula)
display(formula.op) # typed expressions (may) have an operator
display(formula.type) # typed expressions have a type
display(list(formula)) # typed expressions have parts
display(formula.__class__) # the python type of `formula`
'&'
[p_t, q_t]
lamb.meta.BinaryAndExpr
# let's look at a part:
display(formula[0])
display(formula[0].type)
display(formula[0].__class__)
lamb.meta.TypedTerm
Let's look a bit at a metalanguage function. Its parts are the variable, and the body, and its type is guaranteed to be a functional type.
f = %te L x_n : L y_n : x + y
x = %te 2
f
f.type
display(f[0], f[1])
When a function combines with an argument, it builds a composite expression (an "application expression"):
x = %te 2
f(x)
f(x).__class__
lamb.meta.ApplicationExpr
Reduction can be triggered by calling the function reduce()
. (This works on any meta-language object, but most things aren't reducible!)
f(x).reduce().derivation
All of the hard work is done in this function.
What is hard here? Consider the following case (based on an example in Partee, ter Meulen and Wall 1993, Mathematical Methods in Linguistics):
f1 = %te L y_e : L x_e : P_<e,t>(x) & Q_<e,t>(y)
x = %te x_e
display(f1, x, f1(x))
How should we reduce this? Need to alpha-convert the bound variable:
f1(x).reduce_all()
That is, where reduction would result in a name collision between bound variables, the variables must be systematically renamed. The following would be wrong: $\lambda x_e . P(x) \wedge Q(x)$
Pseudocode. Given a LFun
object f
, and an arbitrary TypedExpr
argument a
:
f.type
is compatible with a.type
f[1].subst(f[0], a)
(substitute instances of f[0]
in f[1]
with a
)Where t.subst(var, a)
:
t
is a variable named var
, return a
i
in t
:
a. t[i] = t[i].subst(var, a)
b. return the modified t
Rather subtle to get right! Somewhat imperfect sketch:
blocklist
of variables used in f
or in a
f[0]
and any variables in a
x
not in blocklist
b. systematically replace all bound instances in f[1]
with x
f[1]
, avoiding collisions as well with the renames so far.t2 = %te L x_e : (L y_e : L x_e : P_<e,t>(x) & Q_<e,t>(y))(x_e)
display(t2, t2.reduce_all())
t3 = %te (L x_e : (L y_e : L x_e : P_<e,t>(x) & Q_<e,t>(y))(x_e))(y_e)
display(t3, t3.reduce(), t3.reduce_all())
%%lamb
||two|| = 2
||plus|| = L x_n : L y_n : x + y
What is the nature of these sorts of definitions?
two
and plus
in this example are python objects of class lang.Item
. (lang
is one of the lambda notebook modules.)
Item
is essentially a mapping between a string, representing an object language item, and a single metalanguage object.display(plus.name, plus.content)
'plus'
A python object is a bundle of data and behaviors, specified by its class
plus.name
Example: TypedExpr
is the main metalanguage class, and contains code for type inference, frontend rendering, reduction, variable substitution, etc.
TypedExpr
, which means that they extend its behavior in various ways (while keeping the core behavior).Interesting behavior change: you can change the behavior of most python operators by defining a special class function. E.g. __call__
lets you change what happens when you use the standard function-argument python notation.
Metalanguage TypedExpr
s redefine __call__
so that you can use this notation:
f = %te L x_e : Cat(x)
x = %te A_e
f(x)
INFO (meta): Coerced guessed type for 'Cat_t' into <e,t>, to match argument 'x_e'
The Item
class is a special case of a general abstract type called Composable
. Any object that can undergo composition has this type.
*
operator is implemented, via the __mul__
special function.lamb.lang
module contains a variety of subclasses of Composable
, and mostly you don't need to know which is which.result = plus * two
result
result
here is a python object of class lang.CompositionResult
.
CompositionResult
is a set of mappings between input Composable
s and output metalanguage objects.Composable
.result.results[0].tree()
result.source
'[plus two]'
A CompositionResult
can contain multiple results, representing ambiguity. For example, via lexical ambiguity. Here's the lexical notation to do this in the lambda notebook:
%%lamb
||bank|| = L x_e : Riverbank_<e,t>(x)
||bank[*]|| = L x_e : Moneybank_<e,t>(x)
||the|| = L f_<e,t> : Iota x_e : f(x)
the * bank
Pretty straightforward brute force:
For a * b
a
and b
meet the preconditions of the composition operation O
, add O(a,b)
to the list of results(This is again a Heim & Kratzer + Coppock and Champollion hybrid)
PM: If $\gamma$ is a syntax tree whose subtrees are $\alpha$ and $\beta$, where:
Then, $[[\gamma]] = \lambda x_e . [[\alpha]](x) \wedge [[\beta]](x)$
Primary uses: intersective adjectives, relative clauses
For a * b
a.content
and b.content
have type $\langle e,t \rangle$, then:LFun(x, BinaryAndExpr(a.content(x), b.content(x))
f
= the fully reduced form of that object, and return ||a b|| = f
This is surprisingly annoying to implement!
The idea of PM has a different way it can be expressed:
%te L f_<e,t> : L g_<e,t> : L x_e : f(x) & g(x)
This is what is known as a combinator.
Many composition operations can be expressed as combinators.
If $\gamma$ is a syntax tree whose subtrees are $\alpha$ and $\beta$, and $C$ is a combinator designated as a composition operation, where
Then $[[\gamma]] = C([[\alpha]])([[\beta]])$
This operation is commonly assumed in event semantics, and thought of as either a type-shift or a "unary" operation. For simplicity, I show here a variant of EC that operates on type $e$.
EC: if $[[\alpha]]$ has type $\langle e, X \rangle$ for some type $X$, then $[[EC(\alpha)]] = \exists x_e : [[\alpha]](x)$
The procedural attempt is straightforward, but again somewhat annoying to implement: build an object that introduces a $\exists$ operator.
Let's instead consider a combinator approach, again.
ec_combinator = %te L f_<e,t> : Exists x_e : f(x)
ec_combinator
lang.get_system().add_unary_rule(ec_combinator, "EC")
lang.get_system()
WARNING (lang): Composition rule named 'EC' already present in system, replacing
%lamb ||cat|| = L x_e : Cat_<e,t>(x)
cat.compose()
Existential closure is often instead treated as a type-shift, applying as needed to get type $t$. Unary combinators can also be used for this.
lang.get_system().add_typeshift(ec_combinator, "EC")
lang.get_system().typeshift = True
lang.get_system()
WARNING (lang): Composition rule named 'EC' already present in system, replacing
Let's try it out:
%lamb ||neg|| = L p_t : ~p
neg * cat
(neg * cat).trace()
Still brute force. This is called last-resort type-shifting (Partee):
For a * b
a
and b
meet the preconditions of the composition operation O
, add O(a,b)
to the list of resultst
:Caveat: need to be careful in case type-shifts lead to recursion!
Many composition operations that have been proposed in the literature can be represented as combinators.
The initial core Heim & Kratzer system is essentially the system determined by:
(However -- need to reconsider the types for the first two!)
The "Predicate Abstraction" rule can't be expressed as a combinator.
PA: if $\gamma$ is a branching node with daughters $\alpha_i$ and $\beta$, where $i$ is an LF index, then $[[\gamma]] = \lambda x^i_e . [[\beta]]$
This one is implemented procedurally:
def sbc_pa(binder, content, assignment=None):
if not tree_binder_check(binder):
raise CompositionFailure(binder, content, reason="PA requires a valid binder")
vname = "var%i" % binder.get_index()
bound_var = meta.term(vname, types.type_e)
f = meta.LFun(types.type_e,
content.content.calculate_partiality({bound_var}), vname)
return BinaryComposite(binder, content, f)
binder = lang.Binder(5)
t = lang.Trace(5)
binder * (t * cat)
More operations that can't be handled with combinators:
VAC
operation, which ignores vacuous itemsWhat is a composition system in the end?
Composable
sComposable
s, does a (brute-force) search over possible valid combinationsSwitch to notebook for this:
%%lamb
||cat|| = L x_e: Cat(x)
||gray|| = L x_e: Gray(x)
||kaline|| = Kaline_e
||julius|| = Julius_e
||inP|| = L x_e : L y_e : In(y, x) # `in` is a reserved word in python
||texas|| = Texas_e
||isV|| = L p_<e,t> : p # `is` is a reserved word in python
||fond|| = L x_e : L y_e : Fond(y, x)
of = lang.Item("of", content=None)
a = lang.Item("a", content=None)
binder = lang.Binder(5)
t5 = lang.Trace(5)
display(of, a, binder, t5)
kaline * (isV * (a * (gray * cat)))
kaline * (isV * ((a * (gray * cat)) * (inP * texas)))
kaline * (isV * (a * ((gray * cat) * (inP * texas)
* (binder * (t5 * (fond * (of * julius)))))))
Depending on time, let's look at the Neo-Davidsonian fragment
Next: more on the metalanguage; type inference