具有多态结果值

时间:2018-03-26 13:15:19

标签: haskell polyvariadic

我试图在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

有没有让这个例子工作而不必在这里和那里添加类型注释仍然保持结果类型多态?

2 个答案:

答案 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

然后IOReaderT个实例只是:

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