Ответ 1
Вы были близки к последнему, но вам нужно добавить ограничения:
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ConstraintKinds #-}
import Data.Proxy
both :: (c a, c b)
=> Proxy c
-> (forall x. c x => x -> r)
-> (a, b)
-> (r, r)
both Proxy f (x, y) = (f x, f y)
demo :: (String, String)
demo = both (Proxy :: Proxy Show) show ('a', True)
Proxy
необходимо пройти проверку двусмысленности. Я думаю, что это потому, что в противном случае не было бы известно, какую часть ограничения использовать из этой функции.
Чтобы унифицировать это с другими случаями, вам нужно разрешить пустые ограничения. Возможно, это возможно, но я не уверен. Вы не можете частично применять семейства типов, что может сделать его несколько сложнее.
Это немного более гибко, чем я думал, что это будет:
demo2 :: (Char, Char)
demo2 = both (Proxy :: Proxy ((~) Char)) id ('a', 'b')
Я понятия не имел, что вы можете частично применить равенство типов до этого момента, ха-ха.
К сожалению, это не работает:
demo3 :: (Int, Int)
demo3 = both (Proxy :: Proxy ((~) [a])) length ([1,2,3::Int], "hello")
Для конкретного случая списков мы можем использовать IsList
из GHC.Exts
, чтобы заставить это работать (IsList
обычно используется с расширением OverloadedLists
, но здесь нам это не нужно):
demo3 :: (Int, Int)
demo3 = both (Proxy :: Proxy IsList) (length . toList) ([1,2,3], "hello")
Конечно, самым простым (и даже более общим) решением является использование функции типа (a -> a') -> (b -> b') -> (a, b) -> (a', b')
(например, bimap
from Data.Bifunctor
или (***)
от Control.Arrow
) и просто дважды назначьте эту функцию:
λ> bimap length length ([1,2,3], "hello")
(3,5)
Объединяя все три примера из вопроса
Хорошо, после некоторых размышлений и кодирования я понял, как по крайней мере объединить три примера, которые вы дали в одну функцию. Возможно, это не самая интуитивная вещь, но, похоже, она работает. Фокус в том, что в дополнение к тому, что мы имеем выше, мы позволяем функции возвращать два разных типа результата (элементы результирующей пары могут быть разных типов), если мы дадим системе типов следующее ограничение:
Оба типа результата должны иметь отношение к соответствующему типу ввода, задаваемому классом типа с двумя параметрами (мы можем рассматривать один тип типа параметра как логический предикат для типа, и мы можем посмотреть на класс с двумя типами параметров как захват двоичной связи между двумя типами).
Это необходимо для чего-то вроде applyToTuple (+5) (10 :: Int, 2.3 :: Float)
, так как оно возвращает (Int, Float)
.
При этом получаем:
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
import Data.Proxy
import GHC.Exts
both :: (c a, c b
,p a r1 -- p is a relation between a and r1
,p b r2 -- and also a relation between b and r2
)
=> Proxy c
-> Proxy p
-> (forall r x. (c x, p x r) => x -> r) -- An input type x and a corresponding
-- result type r are valid iff the p from
-- before is a relation between x and r,
-- where x is an instance of c
-> (a, b)
-> (r1, r2)
both Proxy Proxy f (x, y) = (f x, f y)
Proxy p
представляет собой наше отношение между типами ввода и вывода. Затем мы определяем класс удобства (который, насколько я знаю, уже нигде не существует):
class r ~ a => Constant a b r
instance Constant a b a -- We restrict the first and the third type argument to
-- be the same
Это позволяет нам использовать both
, когда тип результата остается неизменным, частично применяя Constant
к типу, который, как мы знаем, он будет (я также не знал, что вы можете частично применять классы классов до сих пор. многому научившись за этот ответ, ха-ха). Например, если мы знаем, что в обоих результатах будет Int
:
example1 :: (Int, Int)
example1 =
both (Proxy :: Proxy IsList) -- The argument must be an IsList instance
(Proxy :: Proxy (Constant Int)) -- The result type must be Int
(length . toList)
([1,2,3], "hello")
Аналогично для вашего второго тестового примера:
example2 :: (String, String)
example2 =
both (Proxy :: Proxy Show) -- The argument must be a Show instance
(Proxy :: Proxy (Constant String)) -- The result type must be String
show
('a', True)
Третий, где он становится немного интереснее:
example3 :: (Int, Float)
example3 =
both (Proxy :: Proxy Num) -- Constrain the the argument to be a Num instance
(Proxy :: Proxy (~)) -- <- Tell the type system that the result type of
-- (+5) is the same as the argument type.
(+5)
(10 :: Int, 2.3 :: Float)
Наше отношение между типом ввода и вывода на самом деле немного сложнее, чем два других примера: вместо игнорирования первого типа в отношении мы говорим, что типы ввода и вывода должны быть одинаковыми (что работает с (+5) :: Num a => a -> a
). Другими словами, в этом конкретном случае наше отношение является отношением равенства.