我试图在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
有没有让这个例子工作而不必在这里和那里添加类型注释仍然保持结果类型多态?
答案 0 :(得分:2)
两个实例重叠,因为它们的索引统一:m' () ~ (a -> r)
m' ~ (->) a
和() ~ r
。
要在m'
不是函数类型时选择第一个实例,您可以添加OVERLAPPING
pragma。 (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为参数write
的上下文中使用m
会很不方便(尝试概括test
的签名。)
有一种方法可以通过使用闭合类型来避免重叠实例,以定义类型级布尔值,当且仅当给定类型是函数类型时才是真的,以便实例可以匹配它。见下文。
它可能只是看起来更像代码和更复杂,但是,除了增加的表达性(我们可以有一个带有test
约束的广义MonadIO
之外,我认为这种风格使得逻辑通过隔离类型上的模式匹配,实例更清晰。
{-# 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用于新的实例终止检查程序。 (我不知道它是否在这里处理这个案子。)
答案 1 :(得分:2)
我在这里充实了我的评论。我们将保留原始类的概念,甚至是现有实例,只添加实例。只需为每个现有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)