В моих объяснениях, как правило, больше слов и меньше формул - извинения, если это бесполезно, но другие ответы меня немного не удовлетворяют, поэтому я подумал, что объясню немного по-другому.
Очевидно, у вас уже есть хорошее представление о том, что делают различные части.В частности, (.)
принимает две функции и дает функцию, которая сначала применяет функцию справа от аргумента, затем применяет функцию слева к результату.
Итак, мы начнем с функциисправа: (+)
.Это имеет тип (Num a) => a -> a -> a
, то есть он принимает два аргумента, которые должны быть одного типа, и этот тип должен быть экземпляром Num
.И затем он возвращает значение того же типа.
Напомним, что из-за карри это эквивалентно (Num a) => a -> (a -> a)
.То есть он принимает экземпляр Num
и возвращает функцию из того же типа самому себе.
Теперь мы должны скомпоновать ее с другой функцией - той, которая принимает тип результата в качестве входных данных.Что это за другая функция?Это снова (+)
!Но мы знаем, что для проверки типа композиции мы должны передать ей тип функции: a -> a
(где a
- это экземпляр Num
).Как мы уже видели, это не проблема, поскольку (+)
может принимать любого типа, который является экземпляром Num
.Это может включать a -> a
, когда у нас есть соответствующий экземпляр.И это объясняет, почему конечный тип имеет 2 ограничения Num a
и Num (a -> a)
.
Теперь все легко собрать воедино.Оставляя без ограничений (с которыми мы уже имели дело), мы схематически имеем:
(+) . (+)
(a -> a) -> ((a -> a) -> (a -> a)) a -> (a -> a)
Итак, в сигнатуре типа (.)
, которую я напишу как (c -> d) -> (b -> c) -> (b -> d)
, мы имеем a
как b
, a -> a
как c
и (a -> a) -> (a -> a)
как d
.Подставляя их в, мы получаем конечный тип (b -> d
) как:
a -> ((a -> a) -> (a -> a))
, который мы можем переписать как
a -> (a -> a) -> a -> a
, удалив ненужные скобки, после вызова функции стрелкиправоассоциативная.