Типовая подпись для функции с возможными полиморфными аргументами

Может ли я, и если да, то как я могу написать сигнатуру типа для функции:

g f x y = (f x, f y)

Такое, что дано:

f1 :: a -> [a]
f1 x = [x]

x1 :: Int
x1 = 42

c1 :: Char
c1 = 'c'

f2 :: Int -> Int
f2 x = 3 * x

x2 :: Int
x2 = 5

Таким образом:

g f1 x1 c1 == ([42], ['c']) :: ([Int], [Char])
g f2 x1 x2 == (126, 15) :: (Int, Int)

Ответы

Ответ 1

Нет, вы не можете. Основная проблема заключается в том, что синтаксис Haskell обманул вас мыслью о том, что типы f1 и f2 более похожи, чем они есть на самом деле. После перевода в GHC Core они выглядят более по-разному:

f1 :: forall a . a -> [a]
f2 :: Int -> Int

Не только это, но соответствующие термины выглядят совсем иначе:

f1 = Λa -> λ(x :: a) -> [x]
f2 = λ(x :: Int) -> 3 * x

Как вы можете видеть, f1 и f2 действительно имеют разные числа аргументов, где f1 принимает тип и значение, а f2 принимает только значение.

В более обычных условиях, когда вы plop f2 в контексте, ожидающем функцию типа, скажем, Int -> [Int], GHC применит f2 к нужному типу для вас (т.е. создать экземпляр f2 к конкретный тип), и все будет хорошо. Например, если у вас есть

g :: (Int -> [Int]) -> Bool

и вы примените g к f, GHC фактически скомпилирует это для

g (f @Int)

Но здесь вам нужен полиморфизм в отношении того, происходит ли это создание или нет, что GHC не поддерживает (я думаю, что это будет довольно радикальное и разрушительное изменение основного языка).

Поскольку экземпляры классов, типы семейных шаблонов и результаты семейства типов не могут быть количественно определены, я уверен, что нет никакого способа получить то, что вы хотите.

Ответ 2

Это действительно возможно, если вы не возражаете добавить аргумент Proxy, используя вариант моего ответа на аналогичный вопрос здесь.

Большая часть моего объяснения из этого ответа содержится здесь, но нам нужно немного расширить его, добавив еще пару классов вспомогательного типа (то, что я называю здесь List и Both здесь):

{-# LANGUAGE RankNTypes            #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeFamilies          #-}
{-# LANGUAGE FlexibleInstances     #-}
{-# LANGUAGE ConstraintKinds       #-}

import           Data.Proxy

f1 :: a -> [a]
f1 x = [x]

x1 :: Int
x1 = 42

c1 :: Char
c1 = 'c'

f2 :: Int -> Int
f2 x = 3 * x

x2 :: Int
x2 = 5

class b ~ [a] => List a b
instance List a [a]

class (a ~ b, b ~ c) => Both a b c
instance Both a a a

g :: (c a r1, c b r2) =>
      Proxy c -> (forall x r. c x r => x -> r) -> a -> b -> (r1, r2)
g _ f x y = (f x, f y)

Это позволяет нам делать

ghci> g (Proxy :: Proxy List) f1 x1 c1
([42],"c")
ghci> g (Proxy :: Proxy (Both Int)) f2 x1 x2
(126,15)

List и Both не являются лучшими именами (особенно List), поэтому вы можете придумать лучшие, если вы это используете (хотя я не уверен, что предлагаю сделать это типа обмана в коде производства в любом случае, если у вас нет действительно веской причины).