具有多态结果值的多元函数

Polyvariadic functions with polymorphic result value

我正在尝试在 Haskell 中将 Pascal 风格的 write 过程作为多元函数来实现。这是一个具有单态结果类型(IO 在这种情况下)的简化版本,效果很好:

{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Main where

import Control.Monad.IO.Class
import Control.Monad.Trans.Reader
import System.IO


class WriteParams a where
    writeParams :: IO () -> a

instance (a ~ ()) => WriteParams (IO a) where
    writeParams = id

instance (Show a, WriteParams r) => WriteParams (a -> r) where
    writeParams m a = writeParams (m >> putStr (show a ++ " "))

write :: WriteParams params => params
write = writeParams (return ())

test :: IO ()
test = do
    write 123
    write ('a', 'z') True

然而,当将结果类型更改为多态类型时,要在具有 MonadIO 实例的不同 monad 中使用该函数,我 运行 会陷入重叠或不可判定的实例。具体来说,以前版本的 a ~ () 技巧不再有效。最好的方法是以下需要大量类型注释的方法:

class WriteParams' m a where
    writeParams' :: m () -> a

instance (MonadIO m, m ~ m') => WriteParams' m (m' ()) where
    writeParams' m = m

instance (MonadIO m, Show a, WriteParams' m r) => WriteParams' m (a -> r) where
    writeParams' m a = writeParams' (m >> liftIO (putStr $ show a ++ " "))

write' :: forall m params . (MonadIO m, WriteParams' m params) => params
write' = writeParams' (return () :: m ())

test' :: IO ()
test' = do
    write' 123 () :: IO ()
    flip runReaderT () $ do
        write' 45 ('a', 'z') :: ReaderT () IO ()
        write' True

有没有办法使这个例子工作而不必到处添加类型注释并且仍然保持结果类型多态性?

这两个实例重叠,因为它们的索引统一:m' () ~ (a -> r)m' ~ (->) a() ~ r

要在 m' 不是函数类型时选择第一个实例,您可以添加 OVERLAPPING 编译指示。 (Read more about it in the GHC user guide)

-- We must put the equality (a ~ ()) to the left to make this
-- strictly less specific than (a -> r)
instance (MonadIO m, a ~ ()) => WriteParams (m a) where
    writeParams = liftIO 

instance {-# OVERLAPPING #-} (Show a, WriteParams r) => WriteParams (a -> r) where
    writeParams m a = writeParams (m >> putStr (show a ++ " "))

然而,重叠实例使得在 monad 是参数 m 的上下文中使用 write 变得不方便(尝试概括 test 的签名)。

有一种方法可以通过使用封闭类型族来避免实例重叠,定义一个 type-level 布尔值当且仅当给定类型是函数类型时为真,以便实例可以匹配它。见下文。

它可以说只是看起来更多的代码和更多的复杂性,但是,除了增加的表现力(我们可以有一个通用的 testMonadIO 约束),我认为这种风格使得通过在类型上隔离 pattern-matching,最终实例的逻辑更加清晰。

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE UndecidableInstances #-}

module Main where

import Control.Monad.IO.Class
import Control.Monad.Trans.Reader
import System.IO


class WriteParams a where
    writeParams :: IO () -> a

instance WriteParamsIf a (IsFun a) => WriteParams a where
    writeParams = writeParamsIf

type family IsFun a :: Bool where
  IsFun (m c) = IsFun1 m
  IsFun a = 'False

type family IsFun1 (f :: * -> *) :: Bool where
  IsFun1 ((->) b) = 'True
  IsFun1 f = 'False

class (isFun ~ IsFun a) => WriteParamsIf a isFun where
  writeParamsIf :: IO () -> a

instance (Show a, WriteParams r) => WriteParamsIf (a -> r) 'True where
  writeParamsIf m a = writeParams (m >> putStr (show a ++ " "))

instance ('False ~ IsFun (m a), MonadIO m, a ~ ()) => WriteParamsIf (m a) 'False where
  writeParamsIf = liftIO

write :: WriteParams params => params
write = writeParams (return ())

test :: (MonadIO m, IsFun1 m ~ 'False) => m ()
test = do
    write 123
    write ('a', 'z') True

main = test  -- for ghc to compile it

关于UndecidableInstances

的一些话

不可判定实例是重叠实例的正交特征,事实上我认为它们的争议要小得多。而使用不当的 OVERLAPPING 可能会导致不连贯(约束在不同的上下文中以不同的方式解决),使用不当的 UndecidableInstances 最坏的情况下可能会使编译器陷入循环(实际上一旦某个阈值达到 GHC 就会终止并显示错误消息达到),这仍然很糟糕,但是当它确实设法解决实例时,它仍然保证解决方案是唯一的。

UndecidableInstances 解除了很久以前有意义的限制,但现在限制太多,无法使用现代扩展来键入 类.

实际上,最常见的类型类和用UndecidableInstances定义的实例,包括上面的,仍然保证它们的解析将终止。事实上,there is an active proposal 用于新的实例终止检查器。 (我还不知道它是否处理这里的情况。)

在这里,我将我的评论充实为一个答案。我们将保留您最初 class 甚至现有实例的想法,仅添加实例。只需为每个现有 MonadIO 个实例添加一个实例;我将只做一个来说明模式。

instance (MonadIO m, a ~ ()) => WriteParams (ReaderT r m a) where
    writeParams = liftIO

一切正常:

main = do
    write 45
    flip runReaderT () $ do
        write 45 ('a', 'z')
        write "hi"

执行时打印 45 45 ('a','z') "hi"

如果您想稍微减少 writeParams = liftIO 样板文件,您可以打开 DefaultSignatures 并添加:

class WriteParams a where
    writeParams :: IO () -> a
    default writeParams :: (MonadIO m, a ~ m ()) => IO () -> a
    writeParams = liftIO

那么 IOReaderT 实例就是:

instance a ~ () => WriteParams (IO a)
instance (MonadIO m, a ~ ()) => WriteParams (ReaderT r m a)