Proper Treatment 正當作法/ blog/ posts/ Translations
標籤 Tags:
2008-08-17 19:19

Brian Friel's ensemble drama “Translations”, set in rural 1833 Ireland, is at the Biltmore Theater with, from left, Geraldine Hughes, Susan Lynch, Alan Cox and Morgan Hallett.

Brian Friel’s play Translations, set in 19th-century Ireland, is about using languages to communicate and control. As you might expect, the relation between Irish and English figures prominently in the play’s plot. It is only fitting that Oleg and I are writing this post in Dublin, at the European Summer School in Logic, Language and Information, because what we want to show you is a nice perspective we learned here last week that relates multiple ways to interpret natural and programming languages. In honor of multilingualism, you can download this post as both a literate Haskell program and an OCaml program. To make sense of this post, you only need to know either Haskell or OCaml.

{-# OPTIONS -fglasgow-exts -fno-monomorphism-restriction #-}

A programming-language researcher and a (natural-language) linguist both aim to relate form to meaning. Usually, there are an infinite number of forms and meanings, so it is impossible to exhaust them in a brute-force list. Instead, it is natural to assume a finite set of building blocks for expressions, then describe systematically the form and meaning of all expressions by specifying the form and meaning of each building block and how they compose. Formally, we define an abstract data type of expressions: the constructors are the building blocks and the observers are a function from expressions to forms and a function from expressions to meanings.

Form ← Expression → Meaning

Linguistics often labels this picture as

Sound ← Syntax → Meaning

or as

Phenogrammatics ← Tectogrammatics → Semantics

whereas computer science usually labels this picture as

Concrete syntax ← Abstract syntax → Semantics

so the left arrow is a pretty printer (whose inverse is a parser) and the right arrow is an evaluator or compiler (whose inverse is a decompiler).

Of course, we want to restrict ourselves to well-formed expressions only, using what some linguists call categories and most computer scientists call types. We also want to restrict the two arrows to produce well-formed forms and well-formed meanings only. In other words, the building blocks should preserve well-formedness.

One way to implement this picture formally is to represent an expression as a data structure, perhaps a tree of building blocks. But then, to enforce well-formedness, it would seem that we need a family of tree types that call for advanced type-system features such as generalized algebraic data types or dependent types. Instead, we will represent an expression as a program that invokes constructors of abstract data types. The latter data types are abstract in that they may be either form or meaning. In other words, we will represent an expression as a program that is parametrically polymorphic in types that are either form or meaning.

A linguistic example

In natural language, two fundamental types are e, for entities, and t, for propositions. Our abstract syntax includes two entities john and mary, two quantifiers everyone and someone, and a transitive verb saw. In Haskell, we define a type class that relates two types e and t. We call this type class Symantics because its interface defines syntax and its instances define semantics.

class Symantics e t | e -> t, t -> e where
  john :: e
  mary :: e
  everyone :: (e -> t) -> t
  someone :: (e -> t) -> t
  saw :: e -> e -> t

In OCaml (or more generally ML), we define a module signature Symantics that includes two types e and t. For example, the function saw maps an object entity and a subject entity (in that order) to a proposition.

module type Symantics = sig
  type e
  type t
  val john: e
  val mary: e
  val everyone: (e -> t) -> t
  val someone : (e -> t) -> t
  val saw: e -> e -> t

Without specifying either form or meaning, we can already construct expressions in the abstract. In Haskell, an abstract expression has an overloaded type. The following definitions each have the type Symantics e t => t because they denote propositions, not entities. For example, test1 is the proposition that John saw Mary.

test1 = saw mary john
test2 = someone (\x -> everyone (\y -> saw y x))
test3 = everyone (\y -> someone (\x -> saw y x))

Analogously in OCaml, an abstract expression is a functor that takes a Symantics module, say S, as input, and produces (a module containing) values whose types are defined in S. The following functor produces values of type S.t because they denote propositions, not entities.

module TEST(S: Symantics) = struct
  open S
  let test1 = saw mary john
  let test2 = someone (fun x -> everyone (fun y -> saw y x))
  let test3 = everyone (fun y -> someone (fun x -> saw y x))

With these test cases in place, it is now time to implement Symantics concretely. The two arrows in the pictures above amount to two implementations. For simplicity, we take the meaning of a proposition to be whether it is true in a model with just two people, John and Mary, who saw each other but not themselves. In Haskell, we provide an instance of the Symantics class.

data Thing = John | Mary

instance Symantics Thing Bool where
  john = John
  mary = Mary
  everyone f = f John && f Mary
  someone  f = f John || f Mary
  saw Mary John = True
  saw John Mary = True
  saw _    _    = False

We can then resolve the overloading in test1, test2, and test3 above to compute their meaning. Indeed, test2 is false because no single person saw everyone—John didn’t see John, and Mary didn’t see Mary—but test3 is true because each person was seen by a (different) person.

> test1 :: Bool
> test2 :: Bool
> test3 :: Bool

Meanwhile in OCaml, we provide a module for the Symantics signature.

module Truth = struct
  type e = John | Mary
  type t = bool
  let john = John
  let mary = Mary
  let everyone f = f John && f Mary
  let someone  f = f John || f Mary
  let saw obj subj = match subj, obj with
        John, Mary | Mary, John -> true
        | _ -> false

We can then instantiate the TEST functor above by this Truth module.

module TT = TEST(Truth);;
let _ = TT.test1;;  (* true  *)
let _ = TT.test2;;  (* false *)
let _ = TT.test3;;  (* true  *)

In programming-language terms, an instance of the Symantics class in Haskell or a module for the Symantics signature in OCaml is an interpreter for the object language whose abstract syntax is defined by the class or signature. In particular, the meaning arrow is an interpreter that evaluates an expression. On the other hand, the form arrow is also an interpreter—one that prints (or pronounces) an expression. We can define it as another Haskell instance

instance Symantics String String where
  john = "John"
  mary = "Mary"
  everyone f = f "everyone"
  someone  f = f "someone"
  saw obj subj = subj ++ " saw " ++ obj

or another OCaml module

module Pheno = struct
    type e = string
    type t = string
    let john = "John"
    let mary = "Mary"
    let everyone f = f "everyone"
    let someone  f = f "someone"
    let saw obj subj = subj ^ " saw " ^ obj

and test it in Haskell

> test1 :: String
"John saw Mary"
> test2 :: String
"someone saw everyone"
> test3 :: String
"someone saw everyone"

or in OCaml

module TP = TEST(Pheno);;
let _ = TP.test1;;  (* "John saw Mary"        *)
let _ = TP.test2;;  (* "someone saw everyone" *)
let _ = TP.test3;;  (* "someone saw everyone" *)

to reveal the well-known scope ambiguity in the English sentence “someone saw everyone”. To wit, test2 and test3 have the same form but we have already seen that they have different meanings.

In these definitions of someone and everyone, linguists will recognize quantifier lowering or quantifier raising, computer scientists will recognize continuation-passing style (CPS), and hey, logicians might even recognize a representation theorem or two.

Exercise: extend the language to include sentences such as “someone’s mother saw everyone’s father” and “Mary knows who John saw”.

Much related work

There is a ton of work in both computer science and linguistics along these lines: to extend the language of abstract expressions, to refine the form and meaning arrows, to simplify the metalanguage in which the interpreters are implemented, to build more interpreters that perform additional processing such as logical deduction or partial evaluation, to compose interpreters, to optimize self-interpretation…

In our recent paper on tagless interpretation, Jacques Carette, Oleg Kiselyov and I tried to survey the most immediately related work, yet we completely missed the connection to natural languages and did not realize that pretty printing is another interpreter. It was at a ESSLLI workshop on type-theoretic grammars, thanks to several talks about abstract categorial grammars (ACGs), that we recognized this connection. Our paper describes tagless partial evaluators and CPS transformations for a programming language. These interpreters do not map function types to function types homomorphically (whereas ACG arrows do). A lobotomized version of this paper is accepted at APLAS 2007; the source code is online.

Finally, multiple interpretations are about to arise in the two ongoing series of posts on this blog.

Stay tuned.