В предыдущей главе мы упёрлись в дилемму: либо пишем все типы руками (надёжно, но занудно), либо просим машину догадаться саму (удобно, но в общем случае неразрешимо). Система Хиндли — Милнера (сокращённо HM) — золотая середина: компилятор выводит самый общий тип любого выражения вообще без аннотаций.
Как удержать полиморфизм типов и не свалиться обратно в неразрешимость System F? Хиндли и Милнер решают это ограничением мест, где может стоять квантор, разнося типы на два этажа: внизу — монотипы — типы без кванторов: атомарные , применения конструкторов типов (так строятся, например, списки и пары) и стрелки. Наверху — схемы (политипы) — монотипы, накрытые сверху кванторами:
Кванторы разрешены только снаружи, в самом начале типа (пренексная форма): — законная схема, а — уже нет: здесь квантор забрался внутрь, слева от стрелки. Разрешить такое — получить полную System F с неразрешимым выводом; запретить — потерять немного выразительности, но превратить вывод типов в алгоритм. Весь фокус HM — в этом ограничении.
Между этажами два перехода, рассмотрим оба. Вниз — инстанцирование (Inst): из схемы делают монотип , подставляя вместо связанной переменной любой монотип. Вверх — обобщение (Gen): если переменная осталась в выведенном типе, но не встречается свободно ни в одном предположении контекста ( ), то ничто снаружи её не ограничивает — на неё можно навесить квантор:
Зачем эти переходы, видно из правила для — сердца всей системы. Связывая имя значением , мы обобщаем его тип до схемы , а дальше каждое использование в теле инстанцирует эту схему заново и независимо от остальных:
У аргумента λ такой привилегии нет. Обобщение происходит только в правиле Let, а параметр функции в него не попадает: когда мы типизируем тело, функция ещё ни к чему не применена, и что придёт ей на вход — неизвестно; всё, что можно сделать, — выдать аргументу одну типовую переменную, общую для всех его использований в теле. Разница видна на двух почти одинаковых термах. Терм типизируется: связан через , обобщён до схемы , и каждое из двух использований инстанцирует её своим типом. Терм — тот же по смыслу — уже не типизируется: здесь — аргумент λ, схемы у него нет, а один монотип не может быть одновременно типом функции над числами и над булевыми значениями.
В Haskell аннотации типов, конечно, есть — сигнатуры можно (и принято) выписывать. Но они необязательны: даже если бы их не было вовсе, компилятор восстановил бы все типы сам — это делает алгоритм W (Дамас — Милнер). Он обходит терм, выдаёт каждому подвыражению свежую типовую переменную, а на каждой аппликации записывает уравнение: тип функции обязан быть стрелкой из типа аргумента в тип результата. Накопленные уравнения решает унификация Робинсона — механическое сопоставление двух типов: чтобы совпало с , достаточно подстановки , . Компилятору хватает голого кода:
compose f g x = f (g x)
-- вывод без единой подсказки:
-- compose :: (b -> c) -> (a -> b) -> a -> cТеперь мы понимаем, почему не получается использовать комбинаторы , и в языке Haskell. Все они построены на самоаппликации — попробуем вывести её тип. Пусть ; раз применяется к самому себе, обязан быть стрелкой из в какой-то — получаем уравнение . Подставим правую часть вместо — и снова внутри: , , … — подстановка зацикливается, тип растёт до бесконечности. Именно это пресекает occurs check — запрет переменной встречаться в типе, который за неё подставляют: без рекурсивных типов у уравнения решения нет. Поэтому рекурсию в Haskell заводят примитивом и рекурсивными определениями.