Haskell: Как написать тип функции из определенного типа в любой тип? - PullRequest
4 голосов
/ 29 октября 2019

В Scala я мог бы написать следующее trait:

trait Consumer[A] {
  def apply(a: A): Unit
}

И scala конвертирует все, что я захочу в Unit, т. Е. Оно будет отбрасывать тип. Эквивалентно, я мог бы сказать, что apply возвращает Any и игнорировать результат.

Однако в Haskell, если бы я определил тип как type Consumer = a -> IO (), я бы не смог передать Int -> IO Int функция, так как Int не ().

Существует два способа решения этой проблемы, но ни один не является удовлетворительным:

  1. Использование Data.Functor.voidна сайте вызова для ручного изменения IO a на IO (). Это раздражает как пользователя API.
  2. define type Consumer a b = a -> IO b, но тогда каждый раз, когда я захочу использовать Consumer в подписи, мне придется носить бесполезный тип b.

Есть ли способ определить тип Consumer как функцию от a до "IO Any"? Насколько я знаю, Haskell не поддерживает что-то вроде exists x. a -> IO x.

Использование forall приводит к противоположному тому, что я хочу, например,

type Consumer = forall b. a -> IO b
foo :: Int -> IO Int
foo = undefined
bar :: Consumer Int
bar = foo

приводит к ошибке:

    • Couldn't match type ‘b’ with ‘Int’
      ‘b’ is a rigid type variable bound by
        the type signature for:
          bar :: Consumer Int
      Expected type: Int -> IO b
        Actual type: Int -> IO Int
    • In the expression: foo
      In an equation for ‘bar’: bar = foo
    • Relevant bindings include
        bar :: Int -> IO b

Обратите внимание, что я специально хочу Consumer для псевдонима be type, а не для конструктора data, как описано здесь: Функция Haskell, возвращающая экзистенциальный тип ,Я бы не возражал, если бы Consumer было class, если бы кто-нибудь знал, как заставить это работать.

Ответы [ 2 ]

0 голосов
/ 31 октября 2019

Чтобы получить экзистенциально-количественный тип в Haskell, вам нужно записать объявление data (в отличие от объявления newtype или объявления псевдонима типа, как вы использовали.).

Вот тип Consumer, который соответствует вашим целям:

{-# LANGUAGE ExistentialQuantification #-}
data Consumer input = forall output. Consumer { runDiscardingOutput :: input -> IO output }

И, аналогично, вот как будет выглядеть ваш пример с новым типом:

f :: Int -> IO Int
f = undefined

g :: Consumer Int
g = Consumer f

Это не такдействительно избегайте ваших опасений по поводу того, что клиентскому коду требуется дополнительный вызов(Я имею в виду, что это не лучше, чем экспортировать привязку consumer = Data.Functor.void из вашей библиотеки.) Кроме того, это также усложняет то, как клиенты смогут использовать потребителя:

consumer :: Consumer Int
consumer = Consumer (\x -> [x])

{- This doesn't typecheck -}
main1 :: IO ()
main1 = runIgnoringOutput consumer 4

{- This doesn't typecheck (!!!) -}
main2 :: IO ()
main2 = void (runIgnoringOutput consumer 4)

{- Only this typechecks :( -}
main3 :: IO ()
main3 =
  case consumer of
  Consumer f -> Data.Functor.void (f 4)

Так что это, вероятно, приведет ксмысл иметь в вашей библиотеке функцию apply, которая выполняла бы грязную работу, точно так же, как в библиотеке Scala была apply функция.

apply :: Consumer a -> a -> IO ()
apply (Consumer f) x = void (f x)
0 голосов
/ 30 октября 2019

Я бы не возражал, если бы Consumer был классом, если бы кто-нибудь знал, как заставить это работать.

Вы можете моделировать экзистенциальные типы для классов со связаннымсемейство типов.

Но Haskell не допускает неоднозначные типы в классах, не используя что-то вроде GADT экзистенциальной оболочки, поэтому у вас все равно будет информация о типе где-то .

{-# LANGUAGE TypeFamilies, MultiParamTypeClasses #-}

class Consumer c a where
  type Output c

  consume :: c -> a -> IO (Output c)

c здесь необходимо для того, чтобы можно было реконструировать тип Output c, поэтому он не является строго экзистенциальным. Но теперь вы можете написать

{-# LANGUAGE FlexibleInstances, InstanceSigs #-}

instance Consumer (a -> IO b) a where
  type Output (a -> IO b) = b

  consume :: (a -> IO b) -> a -> IO b
  consume = id

Это может не подходить для вашего варианта использования, потому что не будет сигнатуры типа, которая может выразить Consumer a по-настоящему экзистенциальным способом. Но можно написать

... :: (Consumer c a) => c -> ...

(Вы также можете использовать FunctionalDependencies здесь, чтобы немного разъяснить класс.)

...