具有多态结果值的多元函数
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 布尔值当且仅当给定类型是函数类型时为真,以便实例可以匹配它。见下文。
它可以说只是看起来更多的代码和更多的复杂性,但是,除了增加的表现力(我们可以有一个通用的 test
和 MonadIO
约束),我认为这种风格使得通过在类型上隔离 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
那么 IO
和 ReaderT
实例就是:
instance a ~ () => WriteParams (IO a)
instance (MonadIO m, a ~ ()) => WriteParams (ReaderT r m a)
我正在尝试在 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 布尔值当且仅当给定类型是函数类型时为真,以便实例可以匹配它。见下文。
它可以说只是看起来更多的代码和更多的复杂性,但是,除了增加的表现力(我们可以有一个通用的 test
和 MonadIO
约束),我认为这种风格使得通过在类型上隔离 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
那么 IO
和 ReaderT
实例就是:
instance a ~ () => WriteParams (IO a)
instance (MonadIO m, a ~ ()) => WriteParams (ReaderT r m a)