The central idea of CCG—and of categorial grammar more generally—is that all the grammatical information resides in the lexicon. There are no phrase structure rules of the form \(\text{S} \rightarrow \text{NP}\;\text{VP}\). Instead, each word carries a complex category that specifies what arguments it takes and what it returns. The combinatory rules are universal—they apply to any categories that have the right shape.
Categories
Categories in CCG are built recursively from a set of atomic types using the forward and backward slash operators:
Atomic categories: \(S\), \(NP\), \(N\), \(PP\), etc.
Complex categories: if \(X\) and \(Y\) are categories, then \(X/Y\) and \(X\backslash Y\) are categories.
A category \(X/Y\) is a function that takes a \(Y\) to its right and returns an \(X\). A category \(X\backslash Y\) is a function that takes a \(Y\) to its left and returns an \(X\).1
For example:
Word
Category
Meaning
the
\(NP/N\)
Takes a noun to the right, yields an NP
greyhound
\(N\)
A noun
loves
\((S\backslash NP)/NP\)
Takes an NP on the right, then an NP on the left, yields S
runs
\(S\backslash NP\)
Takes an NP on the left, yields S
quickly
\((S\backslash NP)\backslash(S\backslash NP)\)
Modifies a VP (takes one on the left, returns one)
Combinatory rules
CCG has a small set of combinatory rules. The simplest are application rules, which are just the elimination rules of the Lambek calculus:
Backward composition (\(<\)B): \[\frac{Y\backslash Z \quad X\backslash Y}{X\backslash Z}\]
And type raising, which turns an argument into a function:
Forward type raising (\(>\)T): \[\frac{X}{T/(T\backslash X)}\]
Backward type raising (\(<\)T): \[\frac{X}{T\backslash(T/X)}\]
Composition and type raising together give CCG the ability to build “non-standard” constituents—groupings of words that wouldn’t be constituents under a CFG analysis—which is exactly what’s needed for phenomena like coordination of unlike categories and extraction.
A CCG parser
Let me implement a basic CCG chart parser. The idea is the same as CKY: fill a chart bottom-up, but instead of CFG rules, use the combinatory rules above.
Define CCG categories
from dataclasses import dataclassfrom typing import Optional@dataclass(frozen=True)class Category:"""A CCG category."""@staticmethoddef parse(s: str) ->'Category':"""Parse a category string like '(S\\NP)/NP'.""" s = s.strip()# Find the main connective (outermost slash not inside parens) depth =0 main_slash =Nonefor i inrange(len(s) -1, -1, -1):if s[i] ==')': depth +=1elif s[i] =='(': depth -=1elif depth ==0and s[i] =='/': main_slash = ('/', i)breakelif depth ==0and s[i] =='\\'and (i ==0or s[i-1] !='\\'): main_slash = ('\\', i)breakif main_slash isNone:# Strip outer parens if presentif s.startswith('(') and s.endswith(')'):return Category.parse(s[1:-1])return AtomicCategory(s) direction, idx = main_slash left = Category.parse(s[:idx]) right = Category.parse(s[idx+1:])if direction =='/':return ForwardSlash(left, right)else:return BackwardSlash(left, right)@dataclass(frozen=True)class AtomicCategory(Category): name: strdef__repr__(self):returnself.name@dataclass(frozen=True)class ForwardSlash(Category): result: Category argument: Categorydef__repr__(self):returnf'({self.result}/{self.argument})'@dataclass(frozen=True)class BackwardSlash(Category): result: Category argument: Categorydef__repr__(self):returnf'({self.result}\\{self.argument})'
class CCGParser:"""A CKY-style chart parser for CCG."""def__init__(self, lexicon: dict[str, list[Category]]):self._lexicon = lexicondef parse(self, words: list[str], goal: Category =None) ->list:"""Parse a list of words. Returns all categories spanning the input."""if goal isNone: goal = AtomicCategory('S') n =len(words)# chart[i][j] = set of categories spanning positions i to j chart = [[set() for _ inrange(n +1)] for _ inrange(n +1)]# Fill lexical entriesfor i, word inenumerate(words):if word inself._lexicon: chart[i][i +1] =set(self._lexicon[word])# Fill chart bottom-upfor span inrange(2, n +1):for i inrange(n - span +1): j = i + spanfor k inrange(i +1, j):for left_cat in chart[i][k]:for right_cat in chart[k][j]:for rule in RULES: result = rule(left_cat, right_cat)if result isnotNone: chart[i][j].add(result)# Check for goal results = [c for c in chart[0][n] if c == goal]return results, chart
# Define a small lexiconlexicon = {'the': [ForwardSlash(AtomicCategory('NP'), AtomicCategory('N'))],'a': [ForwardSlash(AtomicCategory('NP'), AtomicCategory('N'))],'greyhound': [AtomicCategory('N')],'man': [AtomicCategory('N')],'loves': [ForwardSlash(BackwardSlash(AtomicCategory('S'), AtomicCategory('NP')), AtomicCategory('NP'))],'saw': [ForwardSlash(BackwardSlash(AtomicCategory('S'), AtomicCategory('NP')), AtomicCategory('NP'))],'runs': [BackwardSlash(AtomicCategory('S'), AtomicCategory('NP'))],}parser = CCGParser(lexicon)# Parse "the greyhound loves a man"words = ['the', 'greyhound', 'loves', 'a', 'man']results, chart = parser.parse(words)print(f"Input: {' '.join(words)}")print(f"Parses found: {len(results)}")for r in results:print(f" {r}")
Input: the greyhound loves a man
Parses found: 1
S
CCG and the MCS boundary
Vijay-Shanker and Weir (1994) showed that the string languages generated by CCG (with generalized composition of bounded degree) are exactly the same as those generated by TAGs. This means CCG is mildly context-sensitive: it can handle cross-serial dependencies but stays within the polynomial-parsing boundary.
The equivalence is not obvious from the surface—TAGs are tree-based and CCG is type-logical—but it reflects a deep structural fact: both formalisms permit exactly one level of “wrapping” beyond what CFGs can do. The practical consequence is that wide-coverage CCG parsers, like the one developed by Clark and Curran (2007), have the formal power to handle the non-context-free phenomena in natural language while remaining computationally tractable.
References
Clark, Stephen, and James R. Curran. 2007. “Wide-Coverage Efficient Statistical Parsing with CCG and Log-Linear Models.”Computational Linguistics 33 (4): 493–552.
Steedman, Mark. 2000. The Syntactic Process. MIT Press.
Steedman, Mark, and Jason Baldridge. 2011. “Combinatory Categorial Grammar.” In Non-Transformational Syntax. Wiley-Blackwell.
Vijay-Shanker, K., and David J. Weir. 1994. “The Equivalence of Four Extensions of Context-Free Grammars.”Mathematical Systems Theory 27 (6): 511–46.
Footnotes
There are two conventions for the direction of the backslash. I’m following Steedman’s convention, where \(X\backslash Y\) means “give me a \(Y\) on the left to produce an \(X\).” Some authors reverse this.↩︎
---title: Combinatory Categorial Grammarbibliography: ../../references.bibjupyter: python3---Combinatory Categorial Grammar (CCG) extends the [type-logical tradition](type-logical-grammars.qmd) with a specific set of combinatory rules that give it the power to handle the non-context-free phenomena we discussed in the [section on CFG limitations](../cfgs-for-syntax.qmd), while remaining within the [mildly context-sensitive](index.qmd) boundary [@steedman2000syntactic; @steedman2011combinatory].The central idea of CCG—and of categorial grammar more generally—is that all the grammatical information resides in the *lexicon*. There are no phrase structure rules of the form $\text{S} \rightarrow \text{NP}\;\text{VP}$. Instead, each word carries a complex category that specifies what arguments it takes and what it returns. The combinatory rules are universal—they apply to any categories that have the right shape.## CategoriesCategories in CCG are built recursively from a set of atomic types using the forward and backward slash operators:- **Atomic categories**: $S$, $NP$, $N$, $PP$, etc.- **Complex categories**: if $X$ and $Y$ are categories, then $X/Y$ and $X\backslash Y$ are categories.A category $X/Y$ is a function that takes a $Y$ to its right and returns an $X$. A category $X\backslash Y$ is a function that takes a $Y$ to its left and returns an $X$.^[There are two conventions for the direction of the backslash. I'm following Steedman's convention, where $X\backslash Y$ means "give me a $Y$ on the left to produce an $X$." Some authors reverse this.]For example:| Word | Category | Meaning ||------|----------|---------|| the | $NP/N$ | Takes a noun to the right, yields an NP || greyhound | $N$ | A noun || loves | $(S\backslash NP)/NP$ | Takes an NP on the right, then an NP on the left, yields S || runs | $S\backslash NP$ | Takes an NP on the left, yields S || quickly | $(S\backslash NP)\backslash(S\backslash NP)$ | Modifies a VP (takes one on the left, returns one) |## Combinatory rulesCCG has a small set of combinatory rules. The simplest are *application* rules, which are just the elimination rules of the [Lambek calculus](type-logical-grammars.qmd):**Forward application** ($>$):$$\frac{X/Y \quad Y}{X}$$**Backward application** ($<$):$$\frac{Y \quad X\backslash Y}{X}$$Application alone gives us exactly the power of a context-free grammar. What extends CCG beyond context-free is *composition*:**Forward composition** ($>$B):$$\frac{X/Y \quad Y/Z}{X/Z}$$**Backward composition** ($<$B):$$\frac{Y\backslash Z \quad X\backslash Y}{X\backslash Z}$$And *type raising*, which turns an argument into a function:**Forward type raising** ($>$T):$$\frac{X}{T/(T\backslash X)}$$**Backward type raising** ($<$T):$$\frac{X}{T\backslash(T/X)}$$Composition and type raising together give CCG the ability to build "non-standard" constituents—groupings of words that wouldn't be constituents under a CFG analysis—which is exactly what's needed for phenomena like coordination of unlike categories and extraction.## A CCG parserLet me implement a basic CCG chart parser. The idea is the same as [CKY](../../morphological-patterns/cky-parsing.qmd): fill a chart bottom-up, but instead of CFG rules, use the combinatory rules above.```{python}#| code-fold: true#| code-summary: Define CCG categoriesfrom dataclasses import dataclassfrom typing import Optional@dataclass(frozen=True)class Category:"""A CCG category."""@staticmethoddef parse(s: str) ->'Category':"""Parse a category string like '(S\\NP)/NP'.""" s = s.strip()# Find the main connective (outermost slash not inside parens) depth =0 main_slash =Nonefor i inrange(len(s) -1, -1, -1):if s[i] ==')': depth +=1elif s[i] =='(': depth -=1elif depth ==0and s[i] =='/': main_slash = ('/', i)breakelif depth ==0and s[i] =='\\'and (i ==0or s[i-1] !='\\'): main_slash = ('\\', i)breakif main_slash isNone:# Strip outer parens if presentif s.startswith('(') and s.endswith(')'):return Category.parse(s[1:-1])return AtomicCategory(s) direction, idx = main_slash left = Category.parse(s[:idx]) right = Category.parse(s[idx+1:])if direction =='/':return ForwardSlash(left, right)else:return BackwardSlash(left, right)@dataclass(frozen=True)class AtomicCategory(Category): name: strdef__repr__(self):returnself.name@dataclass(frozen=True)class ForwardSlash(Category): result: Category argument: Categorydef__repr__(self):returnf'({self.result}/{self.argument})'@dataclass(frozen=True)class BackwardSlash(Category): result: Category argument: Categorydef__repr__(self):returnf'({self.result}\\{self.argument})'``````{python}#| code-fold: true#| code-summary: CCG combinatory rulesdef forward_application(left: Category, right: Category) -> Optional[Category]:"""X/Y + Y => X"""ifisinstance(left, ForwardSlash) and left.argument == right:return left.resultreturnNonedef backward_application(left: Category, right: Category) -> Optional[Category]:"""Y + X\\Y => X"""ifisinstance(right, BackwardSlash) and right.argument == left:return right.resultreturnNonedef forward_composition(left: Category, right: Category) -> Optional[Category]:"""X/Y + Y/Z => X/Z"""ifisinstance(left, ForwardSlash) andisinstance(right, ForwardSlash):if left.argument == right.result:return ForwardSlash(left.result, right.argument)returnNonedef backward_composition(left: Category, right: Category) -> Optional[Category]:"""Y\\Z + X\\Y => X\\Z"""ifisinstance(left, BackwardSlash) andisinstance(right, BackwardSlash):if right.argument == left.result:return BackwardSlash(right.result, left.argument)returnNoneRULES = [forward_application, backward_application, forward_composition, backward_composition]``````{python}#| code-fold: true#| code-summary: CCG chart parserclass CCGParser:"""A CKY-style chart parser for CCG."""def__init__(self, lexicon: dict[str, list[Category]]):self._lexicon = lexicondef parse(self, words: list[str], goal: Category =None) ->list:"""Parse a list of words. Returns all categories spanning the input."""if goal isNone: goal = AtomicCategory('S') n =len(words)# chart[i][j] = set of categories spanning positions i to j chart = [[set() for _ inrange(n +1)] for _ inrange(n +1)]# Fill lexical entriesfor i, word inenumerate(words):if word inself._lexicon: chart[i][i +1] =set(self._lexicon[word])# Fill chart bottom-upfor span inrange(2, n +1):for i inrange(n - span +1): j = i + spanfor k inrange(i +1, j):for left_cat in chart[i][k]:for right_cat in chart[k][j]:for rule in RULES: result = rule(left_cat, right_cat)if result isnotNone: chart[i][j].add(result)# Check for goal results = [c for c in chart[0][n] if c == goal]return results, chart``````{python}# Define a small lexiconlexicon = {'the': [ForwardSlash(AtomicCategory('NP'), AtomicCategory('N'))],'a': [ForwardSlash(AtomicCategory('NP'), AtomicCategory('N'))],'greyhound': [AtomicCategory('N')],'man': [AtomicCategory('N')],'loves': [ForwardSlash(BackwardSlash(AtomicCategory('S'), AtomicCategory('NP')), AtomicCategory('NP'))],'saw': [ForwardSlash(BackwardSlash(AtomicCategory('S'), AtomicCategory('NP')), AtomicCategory('NP'))],'runs': [BackwardSlash(AtomicCategory('S'), AtomicCategory('NP'))],}parser = CCGParser(lexicon)# Parse "the greyhound loves a man"words = ['the', 'greyhound', 'loves', 'a', 'man']results, chart = parser.parse(words)print(f"Input: {' '.join(words)}")print(f"Parses found: {len(results)}")for r in results:print(f" {r}")```## CCG and the MCS boundary@vijayshanker1994equivalence showed that the string languages generated by CCG (with generalized composition of bounded degree) are exactly the same as those generated by [TAGs](tree-adjoining-grammars.qmd). This means CCG is mildly context-sensitive: it can handle cross-serial dependencies but stays within the polynomial-parsing boundary.The equivalence is not obvious from the surface—TAGs are tree-based and CCG is type-logical—but it reflects a deep structural fact: both formalisms permit exactly one level of "wrapping" beyond what CFGs can do. The practical consequence is that wide-coverage CCG parsers, like the one developed by @clark2007wide, have the formal power to handle the non-context-free phenomena in natural language while remaining computationally tractable.