У меня есть следующий файл:
module SimpleComposition where
class Intermediate a where
f :: a -> Int
g :: Char -> a
h :: Char -> Int
h = f . g
При попытке загрузить его в ghci я получаю сообщение об ошибке:
main.hs:8:5: error:
* No instance for (Intermediate a0) arising from a use of `f'
* In the first argument of `(.)', namely `f'
In the expression: f . g
In an equation for `h': h = f . g
|
8 | h = f . g
| ^
Я считаю, что проблема в том, что кто-то может использовать 2 разных типа, которые являются экземплярами Intermediate
в этой композиции. Как я могу гарантировать, что это то же самое, когда я экспортирую этот модуль?
PS: Это лучший минимальный пример моей проблемы, чем вопрос, который я задавал ранее (Как компоновать полиморфные функции в Haskell?).
Ответы дают решения ошибки, но я думаю, что они не объясняют проблему, вытекающую из ошибки. Итак, позвольте мне попробовать это в качестве дополнения к исправлениям. Предположим, я пишу instance Intermediate () where {f _ = 0; g _ = ();}; instance Intermediate Bool where {f _ = 1; g _ = False;}
. Какое поведение вы ожидаете от h
там?
Проблема не в том, что экземпляр нельзя вывести, а в том, что у компилятора действительно нет возможности узнать, какой тип вам может понадобиться. g
может производить любой запрашиваемый тип (при условии, что у него есть экземпляр Intermediate
), f
может потреблять любой такой тип... но никто не указывает, какой именно.
Но это легко исправить: просто выберите тип сейчас. Конечно, это должен быть тот, у которого есть экземпляр; например если у вас есть
instance Intermediate Char where
f = fromEnum
g = id
тогда вы можете использовать
h :: Char -> Int
h = (f :: Char -> Int) . g
Более лаконичный способ исправить выбор типа — использовать синтаксическое расширение:
{-# LANGUAGE TypeApplications #-}
h = f @Char . g
... или, чтобы подчеркнуть, что вы просто исправляете шрифт посередине,
h = f . id @Char . g
Я считаю, что проблема в том, что кто-то может использовать 2 разных типа, которые являются экземплярами Intermediate в этой композиции.
Нет, проблема в том, что Haskell больше не может извлечь из подписи, что a
использовать. Представьте, что есть два типа Intermediate
:
instance Intermediate Char where
# …
instance Intermediate Bool where
# …
теперь есть две реализации для h
:
h :: Char -> Int
h = f . (g :: Char -> Char)
или:
h :: Char -> Int
h = f . (g :: Char -> Bool)
может быть бесконечное количество типов Intermediate
, которые можно использовать. Проблема в том, что Haskell не может сказать, основываясь на сигнатуре типа, какой тип использовать.
Мы можем дать ему подсказку о типе, но это, конечно, означает, что промежуточный тип является фиксированным.
Простой способ исправить это — использовать asTypeOf :: a -> a -> a. По сути, это функция const
, но два параметра имеют один и тот же тип. Таким образом, это используется для добавления подсказки, какой тип использовать, например:
h :: Intermediate a => a -> Char -> Int
h a x = f (g x `asTypeOf` a)
Таким образом, здесь значение параметра a
не имеет никакого значения, это способ «внедрить» тип, который будет использоваться в качестве типа для результата g
и параметра f
.
Если вы таким образом позже используете h
, вы можете работать с:
h (undefined :: Char) 'a'
чтобы указать, что f
должен иметь тип Char -> Char
, а g
должен иметь тип Char -> Int
.
Как говорят @leftroundabout и @DanielWagner, более чистое решение без использования такой фиктивной переменной — добавить переменную типа в подпись:
{-# LANGUAGE AllowAmbiguousTypes, ScopedTypeVariables, TypeApplications #-}
h :: forall a. Intermediate a => Char -> Int
h = f . g @ a
тогда мы можем использовать h
с переменной типа с:
h @ Char 'a'
Мне не нравится предложение передать фиктивный параметр. По крайней мере, упомяните версию ScopedTypeVariables
/ AllowAmbiguousTypes
(или версию с прокси, если хотите, хотя прокси IMO тоже устарели).
@leftaroundabout: я не уверен AllowAmbiguousTypes
здесь можно решить проблему? Идея заключается в том, чтобы разрешить «доступ» к промежуточному типу, когда мы используем h
как функцию, а не когда мы ее определяем. Таким образом, человек позже может решить, проходят ли данные «через» Char
или Bool
, а не решать, где мы определяем функцию h
.
@WillemVanOnsem Да, AllowAmbiguousTypes
решает эту проблему. (На самом деле, это именно та проблема, для решения которой он был разработан.) Написание h :: Intermediate a => Char -> Int
позволяет звонящему сказать h @String
(или что-то еще).
@DanielWagner: ну, я попробовал это и получил Could not deduce (Intermediate a0) arising from a use of ‘f’ from the context: Intermediate a
, я пробовал это с h = f . id @a . g
и использовал ScopedTypeVariables
, TypeApplications
, но, вероятно, из-за какой-то заморозки, у меня это не работает: repl.it/@willemvo/ неоднозначный#main.hs
@WillemVanOnsem, ты забыл ∀ a
. (Такая заморозка мозгов со мной бы никогда не случилась...).
Лучшей версией подхода с фиктивным аргументом является подход с использованием прокси-аргумента. asProxyTypeOf :: a -> proxy a -> a; asProxyTypeOf a _ = a
. Импортируя Data.Proxy
, вы можете написать a `asProxyTypeOf` (Proxy :: Proxy Int)
или подобное.
Проблема в том, что Haskell не знает, что
a
использовать в качестве промежуточного.