Ключ к этому находится в сигнатуре типа bimap
:
bimap :: Bifunctor p => (a -> b) -> (c -> d) -> p a c -> p b d
В этом конкретном случае, если мы специализируемся p
на SemiDrei a
и переименовываем переменные типа, чтобы избежать путаницы счто a
, мы получаем:
bimap :: (b -> c) -> (d -> e) -> SemiDrei a b d -> SemiDrei a c e
Итак, когда вы пытаетесь реализовать это:
bimap f g = ...
Функции f
и g
полностью произвольны, а не только вих реализация, но также и в их типах ввода и возврата. f
имеет тип b -> c
, где b
и c
могут быть абсолютно любыми - аналогично для g
. Определение, которое вы даете, должно работать абсолютно для любых типов и функций, которые предоставляет вызывающая сторона - это то, что означает (параметрически) полиморфное.
Если мы теперь посмотрим на ваши три определения в этих терминах, мы можем решитькажущаяся тайна:
Первый:
bimap f g (SemiDrei a) = SemiDrei a
это совершенно нормально, как вы видели. SemiDrei a
имеет тип SemiDrei a b c
, где указывается только a
. Это означает, что он может принимать любой тип, например SemiDrei a Int String
, SemiDrei [Bool] (Char, [Double])
или любой другой. SemiDrei a
сам по себе полиморфный, он может быть любого совместимого типа. Это означает, что, в частности, он может действовать как SemiDrei a b c
и SemiDrei a c e
в вышеуказанной подписи bimap
.
Сравните с другими вашими попытками:
bimap f g = id
проблема здесь в том, что id
, хотя и полиморфный, но не полиморфный достаточно для этой цели. Его тип a -> a
(для любого a
), который, в частности, может быть специализирован для SemiDrei a b c -> SemiDrei a b c
. Но он не может быть специализирован для типа SemiDrei a b d -> SemiDrei a c e
, как требуется, потому что b
, c
, d
и e
в общем случае будут совершенно разными типами. Вспомните, что вызывающий абонент из bimap
может выбирать, какие типы - они могут легко выбирать функции f
и g
, где b
и c
, например, разные типы, итогда id
не может принять от SemiDrei a b d
до SemiDrei a c e
, потому что это разные типы.
На этом этапе вы можете возразить, что значение SemiDrei a
может быть значением всехтакие типы. Это совершенно верно, но это не имеет отношения к выводу типов - компилятор заботится только о типах, а не о том, какие значения могут их содержать. Следует учитывать, что разные типы имеют совершенно разные непересекающиеся значения. И, скажем, SemiDrei a Int String
и SemiDrei a Bool Char
на самом деле разные типы. Опять же, компилятор не знает, что Int
и т. Д. На самом деле не используются ни одним из значений типа. Именно поэтому такие «фантомные типы» (типы, которые появляются в определении типа, но не в любом из их конструкторов данных) используются на практике - чтобы компилятор мог различать их по типу, даже если во время выполненияпредставление может быть полностью эквивалентным.
Что касается вашей третьей попытки, bimap f g x = x
, это в точности то же самое, что и предыдущая - она ограничивает bimap f g
тем, что ее тип вывода совпадает с ее вводом. (На самом деле это полностью эквивалентно bimap f g = id
.)
Таким образом, важным выводом является то, что на этапе проверки типов компилятор заботится только о типах - и два типа с разными именами (и должныбыть) считаться совершенно отличным, даже если в обоих случаях могут быть включены эквивалентные значения.