Нормальная техника для функций с несколькими аргументами - как printf
--is использовать рекурсивный класс типов. Для printf
это делается с классом PrintfType
. Важным является понимание рекурсивный экземпляр:
(PrintfArg a, PrintfType r) => PrintfType (a -> r)
Это в основном говорит, что если вы можете вернуть PrintfType
, ваша функция также экземпляр. «Базовый регистр» - это тип типа String
. Итак, если вы хотите позвонить printf
с один аргумент, он запускает два экземпляра: PrintfType String
и PrintfType (a -> r)
, где r
- String
. Если вы хотите два аргументов, он идет: String
, (a -> r)
где r
является String
и (a -> r)
где r
является предыдущей (a -> r)
.
Однако ваша проблема на самом деле немного сложнее. Вы хотите иметь экземпляр, который обрабатывает две задачи. Вы хотите, чтобы ваш экземпляр применим к функциям разных типов (например, V (V a -> V b)
, V (V a -> V b -> V c)
и т. Д.), А также для обеспечения правильного количества аргументов.
Первым шагом для этого является прекращение использования аргументов [String]
. Тип [String]
теряет информацию о том, сколько значений у него есть, поэтому вы не можете проверить наличие соответствующего количества аргументов. Вместо этого вы должны использовать тип для списков аргументов, который отражает количество аргументов.
Этот тип может выглядеть примерно так:
data a :. b = a :. b
это просто тип для объединения двух других типов, которые могут быть использованы, как это:
"foo" :. "bar" :: String :. String
"foo" :. "bar" :. "baz" :: String :. String :. String
Теперь вам просто нужно написать typeclass с рекурсивным экземпляром, который пересекает как список аргументов типа, так и сама функция. Вот очень грубый автономный эскиз того, что я имею в виду; вам придется принять его к вашей конкретной проблеме самостоятельно.
infixr 8 :.
data a :. b = a :. b
class Callable f a b | f -> a b where
call :: V f -> a -> b
instance Callable rf ra (V rb) => Callable (String -> rf) (String :. ra) (V rb) where
call (V f) (a :. rest) = call (V (f a)) rest
instance Callable String() (V String) where
call (V f)() = V f
Вы также должны включить несколько расширений: FlexibleInstances
, FucntionalDepenedencies
и UndecidableInstances
.
Вы могли бы использовать его как это:
*Main> call (V "foo")()
V "foo"
*Main> call (V (\ x -> "foo " ++ x)) ("bar" :.())
V "foo bar"
*Main> call (V (\ x y -> "foo " ++ x ++ y)) ("bar" :. " baz" :.())
V "foo bar baz"
Если вы передаете в неправильном количестве аргументов, вы получите ошибку типа. По общему признанию, это не самое приятное сообщение об ошибке в мире! Тем не менее, важная часть ошибки (Couldn't match type `()' with `[Char] :.()'
) делает, указывая на основную проблему (списки аргументов, которые не совпадают), что должно быть достаточно простым, чтобы следовать.
*Main> call (V (\ x -> "foo " ++ x)) ("bar" :. "baz" :.())
<interactive>:101:1:
Couldn't match type `()' with `[Char] :.()'
When using functional dependencies to combine
Callable String() (V String),
arising from the dependency `f -> a b'
in the instance declaration at /home/tikhon/Documents/so/call.hs:16:14
Callable [Char] ([Char] :.()) (V [Char]),
arising from a use of `call' at <interactive>:101:1-4
In the expression:
call (V (\ x -> "foo " ++ x)) ("bar" :. "baz" :.())
In an equation for `it':
it = call (V (\ x -> "foo " ++ x)) ("bar" :. "baz" :.())
Обратите внимание, что это может быть немного более сложным, для конкретной задачи - я не уверен, что это решение лучше к этой проблеме. Но это очень хорошее упражнение для обеспечения более сложных инвариантов на уровне типов с использованием более продвинутых функций класса.
Имейте в виду, что 'V a -> V b -> V c' на самом деле' V a -> (V b -> V c) '. Вы можете сделать это через индукцию. Базовый регистр: 'V a' является вызываемым. Индуктивный случай: при условии, что 'V a' является вызываемым, тогда' V b -> V a' является вызываемым. Глава экземпляра индуктивного случая будет иметь общую форму 'instance Callable k => Callable (m -> k) где'. –