Система Хиндли — Милнера

В предыдущей главе мы упёрлись в дилемму: либо пишем все типы руками (надёжно, но занудно), либо просим машину догадаться саму (удобно, но в общем случае неразрешимо). Система Хиндли — Милнера (сокращённо HM) — золотая середина: компилятор выводит самый общий тип любого выражения вообще без аннотаций.

Как удержать полиморфизм типов и не свалиться обратно в неразрешимость System F? Хиндли и Милнер решают это ограничением мест, где может стоять квантор, разнося типы на два этажа: внизу — монотипы τ\tau — типы без кванторов: атомарные α\alpha, применения конструкторов типов Cτ1τnC\,\tau_1 \ldots \tau_n (так строятся, например, списки и пары) и стрелки. Наверху — схемы (политипы) σ\sigma — монотипы, накрытые сверху кванторами:

τ::=αCτ1τnτ\displaystyle \tau \mathrel{\text{::=}} \alpha \mid C\,\tau_1 \ldots \tau_n \mid \tau τ,σ::=τα.σ\displaystyle \to \tau, \qquad \sigma \mathrel{\text{::=}} \tau \mid \forall \alpha.\, \sigma
(1.39)

Кванторы разрешены только снаружи, в самом начале типа (пренексная форма): α.α\forall \alpha.\, \alpha α\to \alpha — законная схема, а (α.αα)(\forall \alpha.\, \alpha \to \alpha) β\to \beta — уже нет: здесь квантор забрался внутрь, слева от стрелки. Разрешить такое — получить полную System F с неразрешимым выводом; запретить — потерять немного выразительности, но превратить вывод типов в алгоритм. Весь фокус HM — в этом ограничении.

Между этажами два перехода, рассмотрим оба. Вниз — инстанцирование (Inst): из схемы делают монотип σ[α:=τ]\sigma[\alpha := \tau], подставляя вместо связанной переменной любой монотип. Вверх — обобщение (Gen): если переменная α\alpha осталась в выведенном типе, но не встречается свободно ни в одном предположении контекста (α\alpha free(Γ)\notin \mathrm{free}(\Gamma)), то ничто снаружи её не ограничивает — на неё можно навесить квантор:

Γe:α.σΓe:σ[α:=τ]  (Inst),Γe:σαfree(Γ)Γe:α.σ  (Gen)\frac{\Gamma \vdash e : \forall \alpha.\, \sigma}{\Gamma \vdash e : \sigma[\alpha := \tau]}\;(\mathrm{Inst}), \qquad \frac{\Gamma \vdash e : \sigma \quad \alpha \notin \mathrm{free}(\Gamma)}{\Gamma \vdash e : \forall \alpha.\, \sigma}\;(\mathrm{Gen})
(1.40)

Зачем эти переходы, видно из правила для let\mathbf{let} — сердца всей системы. Связывая имя xx значением e0e_0, мы обобщаем его тип до схемы σ\sigma, а дальше каждое использование xx в теле e1e_1 инстанцирует эту схему заново и независимо от остальных:

Γe0:σΓ,  x:σe1:τΓlet  x=e0  in  e1:τ  (Let)\frac{\Gamma \vdash e_0 : \sigma \qquad \Gamma,\; x{:}\sigma \vdash e_1 : \tau}{\Gamma \vdash \mathbf{let}\; x = e_0\; \mathbf{in}\; e_1 : \tau}\;(\mathrm{Let})
(1.41)

У аргумента λ такой привилегии нет. Обобщение происходит только в правиле Let, а параметр функции в него не попадает: когда мы типизируем тело, функция ещё ни к чему не применена, и что придёт ей на вход — неизвестно; всё, что можно сделать, — выдать аргументу одну типовую переменную, общую для всех его использований в теле. Разница видна на двух почти одинаковых термах. Терм let  id\mathbf{let}\;id =λy.y  in  (id  3,id  true)= \lambda y.\,y\;\mathbf{in}\;(id\;\overline{3},\, id\;\mathbf{true}) типизируется: idid связан через let\mathbf{let}, обобщён до схемы α.α\forall\alpha.\,\alpha α\to\alpha, и каждое из двух использований инстанцирует её своим типом. Терм (λid.(id  3,id  true))(λy.y)(\lambda id.\,(id\;\overline{3},\, id\;\mathbf{true}))\,(\lambda y.\,y) — тот же по смыслу — уже не типизируется: здесь idid — аргумент λ, схемы у него нет, а один монотип не может быть одновременно типом функции над числами и над булевыми значениями.

В Haskell аннотации типов, конечно, есть — сигнатуры можно (и принято) выписывать. Но они необязательны: даже если бы их не было вовсе, компилятор восстановил бы все типы сам — это делает алгоритм W (Дамас — Милнер). Он обходит терм, выдаёт каждому подвыражению свежую типовую переменную, а на каждой аппликации записывает уравнение: тип функции обязан быть стрелкой из типа аргумента в тип результата. Накопленные уравнения решает унификация Робинсона — механическое сопоставление двух типов: чтобы α\alpha β\to \beta совпало с (γγ)(\gamma\to\gamma) δ\to \delta, достаточно подстановки α:\alpha : =γ= \gamma γ\to\gamma, β:\beta : =δ= \delta. Компилятору хватает голого кода:

compose f g x = f (g x)

-- вывод без единой подсказки:
-- compose :: (b -> c) -> (a -> b) -> a -> c

Теперь мы понимаем, почему не получается использовать комбинаторы Y\mathrm{Y}, Θ\Theta и Z\mathrm{Z} в языке Haskell. Все они построены на самоаппликации xxx\,x — попробуем вывести её тип. Пусть x:αx : \alpha; раз xx применяется к самому себе, α\alpha обязан быть стрелкой из α\alpha в какой-то β\beta — получаем уравнение α\alpha =α= \alpha β\to \beta. Подставим правую часть вместо α\alpha — и α\alpha снова внутри: (αβ)(\alpha\to\beta) β\to\beta, ((αβ)β)((\alpha\to\beta)\to\beta) β\to\beta, … — подстановка зацикливается, тип растёт до бесконечности. Именно это пресекает occurs check — запрет переменной встречаться в типе, который за неё подставляют: без рекурсивных типов у уравнения решения нет. Поэтому рекурсию в Haskell заводят примитивом fix\mathtt{fix} и рекурсивными определениями.