0

我试图write在 Haskell 中实现一个 Pascal 风格的过程作为一个多变量函数。这是具有单态结果类型(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

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

4

2 回答 2

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

那么IOandReaderT实例就是:

instance a ~ () => WriteParams (IO a)
instance (MonadIO m, a ~ ()) => WriteParams (ReaderT r m a)
于 2018-03-26T19:49:47.723 回答
2

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

m'要在不是函数类型时选择第一个实例,您可以添加一个OVERLAPPINGpragma。(在 GHC 用户指南中了解更多信息

-- 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 ++ " "))

但是,重叠实例使得write在 monad 是参数的上下文中使用不方便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,包括上面的一个,仍然保证它们的解析将终止。事实上,一个新的实例终止检查器的积极提案。(我还不知道它是否在这里处理这种情况。)

于 2018-03-26T13:45:28.747 回答